NGA Follow Support

同步客户端关注功能

当前为 2024-03-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Follow Support
  3. // @namespace https://gf.qytechs.cn/users/263018
  4. // @version 1.3.4
  5. // @author snyssss
  6. // @description 同步客户端关注功能
  7.  
  8. // @match *://bbs.nga.cn/*
  9. // @match *://ngabbs.com/*
  10. // @match *://nga.178.com/*
  11.  
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_addValueChangeListener
  16. // @grant GM_registerMenuCommand
  17.  
  18. // @noframes
  19. // ==/UserScript==
  20.  
  21. ((ui, self) => {
  22. if (!ui || !self) return;
  23.  
  24. // 钩子
  25. const hookFunction = (object, functionName, callback) => {
  26. ((originalFunction) => {
  27. object[functionName] = function () {
  28. const returnValue = originalFunction.apply(this, arguments);
  29.  
  30. callback.apply(this, [returnValue, originalFunction, arguments]);
  31.  
  32. return returnValue;
  33. };
  34. })(object[functionName]);
  35. };
  36.  
  37. // 用户信息
  38. class UserInfo {
  39. execute(task) {
  40. task().finally(() => {
  41. if (this.waitingQueue.length) {
  42. const next = this.waitingQueue.shift();
  43.  
  44. this.execute(next);
  45. } else {
  46. this.isRunning = false;
  47. }
  48. });
  49. }
  50.  
  51. enqueue(task) {
  52. if (this.isRunning) {
  53. this.waitingQueue.push(task);
  54. } else {
  55. this.isRunning = true;
  56.  
  57. this.execute(task);
  58. }
  59. }
  60.  
  61. rearrange() {
  62. if (this.data) {
  63. const list = Object.values(this.children);
  64.  
  65. for (let i = 0; i < list.length; i++) {
  66. if (list[i].source === undefined) {
  67. list[i].create(this.data);
  68. }
  69.  
  70. Object.entries(this.container).forEach((item) => {
  71. list[i].clone(this.data, item);
  72. });
  73. }
  74. }
  75. }
  76.  
  77. reload() {
  78. this.enqueue(async () => {
  79. this.data = await get_user_info(this.uid);
  80.  
  81. Object.values(this.children).forEach((item) => item.destroy());
  82.  
  83. this.rearrange();
  84. });
  85. }
  86.  
  87. constructor(id) {
  88. this.uid = id;
  89.  
  90. this.waitingQueue = [];
  91. this.isRunning = false;
  92.  
  93. this.container = {};
  94. this.children = {};
  95.  
  96. this.reload();
  97. }
  98. }
  99.  
  100. // 用户信息组件
  101. class UserInfoWidget {
  102. destroy() {
  103. if (this.source) {
  104. this.source = undefined;
  105. }
  106.  
  107. if (this.target) {
  108. Object.values(this.target).forEach((item) => {
  109. if (item.parentNode) {
  110. item.parentNode.removeChild(item);
  111. }
  112. });
  113. }
  114. }
  115.  
  116. clone(data, [argid, container]) {
  117. if (this.source) {
  118. if (this.target[argid] === undefined) {
  119. this.target[argid] = this.source.cloneNode(true);
  120.  
  121. if (this.callback) {
  122. this.callback(data, this.target[argid]);
  123. }
  124. }
  125.  
  126. const isSmall = container.classList.contains("posterInfoLine");
  127.  
  128. if (isSmall) {
  129. const anchor = container.querySelector(".author ~ br");
  130.  
  131. if (anchor) {
  132. anchor.parentNode.insertBefore(this.target[argid], anchor);
  133. }
  134. } else {
  135. container.appendChild(this.target[argid]);
  136. }
  137. }
  138. }
  139.  
  140. constructor(func, callback) {
  141. this.create = (data) => {
  142. this.destroy();
  143.  
  144. this.source = func(data);
  145. this.target = {};
  146. };
  147.  
  148. this.callback = callback;
  149. }
  150. }
  151.  
  152. ui.sn = ui.sn || {};
  153. ui.sn.userInfo = ui.sn.userInfo || {};
  154.  
  155. ((info) => {
  156. // 扩展规则
  157. const extraData = (() => {
  158. const key = `EXTRA_DATA`;
  159. const data = GM_getValue(key) || {
  160. [0]: {
  161. time: 0,
  162. },
  163. };
  164.  
  165. const save = () => {
  166. GM_setValue(key, data);
  167. };
  168.  
  169. const setValue = (uid, value) => {
  170. data[uid] = value;
  171.  
  172. save();
  173. };
  174.  
  175. const getValue = (uid) => data[uid];
  176.  
  177. const remove = (uid) => {
  178. delete data[uid];
  179.  
  180. save();
  181. };
  182.  
  183. const specialList = () => {
  184. const result = Object.entries(data).filter(
  185. ([key, value]) => value.level
  186. );
  187.  
  188. if (Object.keys(result)) {
  189. return result;
  190. }
  191.  
  192. return null;
  193. };
  194.  
  195. GM_addValueChangeListener(key, function (_, prev, next) {
  196. Object.assign(data, next);
  197. });
  198.  
  199. return {
  200. specialList,
  201. setValue,
  202. getValue,
  203. remove,
  204. };
  205. })();
  206.  
  207. // 获取用户信息
  208. const get_user_info = (uid) => {
  209. const searchPair = (content, keyword, start = "{", end = "}") => {
  210. // 获取成对括号的位置
  211. const getLastIndex = (content, position, start = "{", end = "}") => {
  212. if (position >= 0) {
  213. let nextIndex = position + 1;
  214.  
  215. while (nextIndex < content.length) {
  216. if (content[nextIndex] === end) {
  217. return nextIndex;
  218. }
  219.  
  220. if (content[nextIndex] === start) {
  221. nextIndex = getLastIndex(content, nextIndex, start, end);
  222.  
  223. if (nextIndex < 0) {
  224. break;
  225. }
  226. }
  227.  
  228. nextIndex = nextIndex + 1;
  229. }
  230. }
  231.  
  232. return -1;
  233. };
  234.  
  235. // 起始位置
  236. const str = keyword + start;
  237.  
  238. // 起始下标
  239. const index = content.indexOf(str) + str.length;
  240.  
  241. // 结尾下标
  242. const lastIndex = getLastIndex(content, index, start, end);
  243.  
  244. if (lastIndex >= 0) {
  245. return start + content.substring(index, lastIndex) + end;
  246. }
  247.  
  248. return null;
  249. };
  250.  
  251. return new Promise((resolve, reject) => {
  252. fetch(`/nuke.php?func=ucp&uid=${uid}`)
  253. .then((res) => res.blob())
  254. .then((blob) => {
  255. const reader = new FileReader();
  256.  
  257. reader.onload = () => {
  258. const text = searchPair(reader.result, `__UCPUSER =`);
  259.  
  260. if (text) {
  261. try {
  262. resolve(JSON.parse(text));
  263. return;
  264. } catch {
  265. reject();
  266. return;
  267. }
  268. }
  269.  
  270. reject();
  271. };
  272.  
  273. reader.readAsText(blob, "GBK");
  274. })
  275. .catch(() => {
  276. reject();
  277. });
  278. });
  279. };
  280.  
  281. // 获取用户发帖列表
  282. const get_user_topic_list = (uid) =>
  283. new Promise((resolve) => {
  284. fetch(`/thread.php?lite=js&authorid=${uid}`)
  285. .then((res) => res.blob())
  286. .then((blob) => {
  287. const reader = new FileReader();
  288.  
  289. reader.onload = () => {
  290. const text = reader.result;
  291. const result = eval(`
  292. (${text.replace("window.script_muti_get_var_store=", "")})
  293. `);
  294.  
  295. if (result.data) {
  296. resolve(result.data.__T);
  297. } else {
  298. resolve({});
  299. }
  300. };
  301.  
  302. reader.readAsText(blob, "GBK");
  303. })
  304. .catch(() => {
  305. resolve({});
  306. });
  307. });
  308.  
  309. // 获取用户回帖列表
  310. const get_user_post_list = (uid) =>
  311. new Promise((resolve, reject) => {
  312. fetch(`/thread.php?lite=js&authorid=${uid}&searchpost=1`)
  313. .then((res) => res.blob())
  314. .then((blob) => {
  315. const reader = new FileReader();
  316.  
  317. reader.onload = () => {
  318. const text = reader.result;
  319. const result = eval(`
  320. (${text.replace("window.script_muti_get_var_store=", "")})
  321. `);
  322.  
  323. if (result.data) {
  324. resolve(result.data.__T);
  325. } else {
  326. resolve({});
  327. }
  328. };
  329.  
  330. reader.readAsText(blob, "GBK");
  331. })
  332. .catch(() => {
  333. resolve({});
  334. });
  335. });
  336.  
  337. // 关注
  338. const follow = (uid) =>
  339. new Promise((resolve, reject) => {
  340. fetch(
  341. `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=1`,
  342. {
  343. method: "post",
  344. }
  345. )
  346. .then((res) => res.blob())
  347. .then((blob) => {
  348. const reader = new FileReader();
  349.  
  350. reader.onload = () => {
  351. const text = reader.result;
  352. const result = eval(`
  353. (${text.replace("window.script_muti_get_var_store=", "")})
  354. `);
  355.  
  356. if (result.data) {
  357. resolve(result.data[0]);
  358. } else {
  359. reject(result.error[0]);
  360. }
  361. };
  362.  
  363. reader.readAsText(blob, "GBK");
  364. })
  365. .catch(() => {
  366. reject();
  367. });
  368. });
  369.  
  370. // 取消关注
  371. const un_follow = (uid) =>
  372. new Promise((resolve, reject) => {
  373. fetch(
  374. `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=8`,
  375. {
  376. method: "post",
  377. }
  378. )
  379. .then((res) => res.blob())
  380. .then((blob) => {
  381. const reader = new FileReader();
  382.  
  383. reader.onload = () => {
  384. const text = reader.result;
  385. const result = eval(`
  386. (${text.replace("window.script_muti_get_var_store=", "")})
  387. `);
  388.  
  389. if (result.data) {
  390. resolve(result.data[0]);
  391. } else {
  392. reject(result.error[0]);
  393. }
  394. };
  395.  
  396. reader.readAsText(blob, "GBK");
  397. })
  398. .catch(() => {
  399. reject();
  400. });
  401. });
  402.  
  403. // 移除粉丝
  404. const un_follow_fans = (uid) =>
  405. new Promise((resolve, reject) => {
  406. fetch(
  407. `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=256`,
  408. {
  409. method: "post",
  410. }
  411. )
  412. .then((res) => res.blob())
  413. .then((blob) => {
  414. const reader = new FileReader();
  415.  
  416. reader.onload = () => {
  417. const text = reader.result;
  418. const result = eval(`
  419. (${text.replace("window.script_muti_get_var_store=", "")})
  420. `);
  421.  
  422. if (result.data) {
  423. resolve(result.data[0]);
  424. } else {
  425. reject(result.error[0]);
  426. }
  427. };
  428.  
  429. reader.readAsText(blob, "GBK");
  430. })
  431. .catch(() => {
  432. reject();
  433. });
  434. });
  435.  
  436. // 获取关注列表
  437. const follow_list = (page) =>
  438. new Promise((resolve, reject) => {
  439. fetch(
  440. `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow&page=${page}`,
  441. {
  442. method: "post",
  443. }
  444. )
  445. .then((res) => res.blob())
  446. .then((blob) => {
  447. const reader = new FileReader();
  448.  
  449. reader.onload = () => {
  450. const text = reader.result;
  451. const result = eval(`
  452. (${text.replace("window.script_muti_get_var_store=", "")})
  453. `);
  454.  
  455. if (result.data) {
  456. resolve(result.data[0]);
  457. } else {
  458. reject(result.error[0]);
  459. }
  460. };
  461.  
  462. reader.readAsText(blob, "GBK");
  463. })
  464. .catch(() => {
  465. reject();
  466. });
  467. });
  468.  
  469. // 获取粉丝列表
  470. const follow_by_list = (page) =>
  471. new Promise((resolve, reject) => {
  472. fetch(
  473. `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow_by&page=${page}`,
  474. {
  475. method: "post",
  476. }
  477. )
  478. .then((res) => res.blob())
  479. .then((blob) => {
  480. const reader = new FileReader();
  481.  
  482. reader.onload = () => {
  483. const text = reader.result;
  484. const result = eval(`
  485. (${text.replace("window.script_muti_get_var_store=", "")})
  486. `);
  487.  
  488. if (result.data) {
  489. resolve(result.data[0]);
  490. } else {
  491. reject(result.error[0]);
  492. }
  493. };
  494.  
  495. reader.readAsText(blob, "GBK");
  496. })
  497. .catch(() => {
  498. reject();
  499. });
  500. });
  501.  
  502. // 获取关注动态
  503. const follow_dymanic_list = (page) =>
  504. new Promise((resolve, reject) => {
  505. fetch(
  506. `/nuke.php?lite=js&__lib=follow_v2&__act=get_push_list&page=${page}`,
  507. {
  508. method: "post",
  509. }
  510. )
  511. .then((res) => res.blob())
  512. .then((blob) => {
  513. const reader = new FileReader();
  514.  
  515. reader.onload = () => {
  516. const text = reader.result;
  517. const result = eval(`
  518. (${text.replace("window.script_muti_get_var_store=", "")})
  519. `);
  520.  
  521. if (result.data) {
  522. resolve(result.data);
  523. } else {
  524. reject(result.error[0]);
  525. }
  526. };
  527.  
  528. reader.readAsText(blob, "GBK");
  529. })
  530. .catch(() => {
  531. reject();
  532. });
  533. });
  534.  
  535. // 切换关注
  536. const handleSwitchFollow = (uid, isFollow) => {
  537. if (isFollow) {
  538. if (confirm("取消关注?")) {
  539. un_follow(uid).then(() => {
  540. info[uid]?.reload();
  541. u.refresh();
  542. });
  543. }
  544. } else {
  545. follow(uid).then(() => {
  546. info[uid]?.reload();
  547. u.refresh();
  548. });
  549. }
  550. };
  551.  
  552. // 移除粉丝
  553. const handleRemoveFans = (uid) => {
  554. if (confirm("移除粉丝?")) {
  555. un_follow_fans(uid).then(() => {
  556. u.refresh();
  557. });
  558. }
  559. };
  560.  
  561. // STYLE
  562. GM_addStyle(`
  563. .s-user-info-container:not(:hover) .ah {
  564. display: none !important;
  565. }
  566. .s-table-wrapper {
  567. height: calc((2em + 10px) * 11 + 3px);
  568. overflow-y: auto;
  569. }
  570. .s-table {
  571. margin: 0;
  572. }
  573. .s-table th,
  574. .s-table td {
  575. position: relative;
  576. white-space: nowrap;
  577. }
  578. .s-table th {
  579. position: sticky;
  580. top: 2px;
  581. z-index: 1;
  582. }
  583. .s-table input:not([type]), .s-table input[type="text"] {
  584. margin: 0;
  585. box-sizing: border-box;
  586. height: 100%;
  587. width: 100%;
  588. }
  589. .s-input-wrapper {
  590. position: absolute;
  591. top: 6px;
  592. right: 6px;
  593. bottom: 6px;
  594. left: 6px;
  595. }
  596. .s-text-ellipsis {
  597. display: flex;
  598. }
  599. .s-text-ellipsis > * {
  600. flex: 1;
  601. width: 1px;
  602. overflow: hidden;
  603. text-overflow: ellipsis;
  604. }
  605. .s-button-group {
  606. margin: -.1em -.2em;
  607. }
  608. `);
  609.  
  610. // MENU
  611. const m = (() => {
  612. const container = document.createElement("DIV");
  613.  
  614. container.className = `td`;
  615. container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">关注</a>`;
  616.  
  617. const content = container.querySelector("A");
  618.  
  619. const create = (onclick) => {
  620. const anchor = document.querySelector("#mainmenu .td:last-child");
  621.  
  622. anchor.before(container);
  623.  
  624. content.onclick = onclick;
  625. };
  626.  
  627. const update = (count) => {
  628. if (count) {
  629. content.innerHTML = `关注 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  630. } else {
  631. content.innerHTML = `关注`;
  632. }
  633. };
  634.  
  635. return {
  636. create,
  637. update,
  638. };
  639. })();
  640.  
  641. // UI
  642. const u = (() => {
  643. const modules = {};
  644.  
  645. const createView = () => {
  646. const tabContainer = (() => {
  647. const c = document.createElement("div");
  648.  
  649. c.className = "w100";
  650. c.innerHTML = `
  651. <div class="right_" style="margin-bottom: 5px;">
  652. <table class="stdbtn" cellspacing="0">
  653. <tbody>
  654. <tr></tr>
  655. </tbody>
  656. </table>
  657. </div>
  658. <div class="clear"></div>
  659. `;
  660.  
  661. return c;
  662. })();
  663.  
  664. const tabPanelContainer = (() => {
  665. const c = document.createElement("div");
  666.  
  667. c.style = "width: 800px;";
  668.  
  669. return c;
  670. })();
  671.  
  672. const content = (() => {
  673. const c = document.createElement("div");
  674.  
  675. c.append(tabContainer);
  676. c.append(tabPanelContainer);
  677.  
  678. return c;
  679. })();
  680.  
  681. const addModule = (() => {
  682. const tc = tabContainer.getElementsByTagName("tr")[0];
  683. const cc = tabPanelContainer;
  684.  
  685. return (module) => {
  686. const tabBox = document.createElement("td");
  687.  
  688. tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
  689.  
  690. const tab = tabBox.childNodes[0];
  691.  
  692. const toggle = () => {
  693. Object.values(modules).forEach((item) => {
  694. if (item.tab === tab) {
  695. item.tab.className = "nobr";
  696. item.content.style = "display: block";
  697. item.visible = true;
  698. } else {
  699. item.tab.className = "nobr silver";
  700. item.content.style = "display: none";
  701. item.visible = false;
  702. }
  703. });
  704.  
  705. module.refresh();
  706. };
  707.  
  708. tc.append(tabBox);
  709. cc.append(module.content);
  710.  
  711. tab.onclick = (() => {
  712. let timeout;
  713.  
  714. return () => {
  715. if (timeout > 0) {
  716. return;
  717. }
  718.  
  719. timeout = setTimeout(() => {
  720. timeout = 0;
  721. }, 320);
  722.  
  723. toggle();
  724. };
  725. })();
  726.  
  727. modules[module.name] = {
  728. ...module,
  729. tab,
  730. toggle,
  731. visible: false,
  732. };
  733.  
  734. return modules[module.name];
  735. };
  736. })();
  737.  
  738. return {
  739. content,
  740. addModule,
  741. };
  742. };
  743.  
  744. const refresh = () => {
  745. Object.values(modules)
  746. .find((item) => item.visible)
  747. ?.refresh();
  748. };
  749.  
  750. return {
  751. modules,
  752. createView,
  753. refresh,
  754. };
  755. })();
  756.  
  757. // 我的关注
  758. {
  759. const name = "我的关注";
  760.  
  761. const content = (() => {
  762. const c = document.createElement("div");
  763.  
  764. c.style.display = "none";
  765. c.innerHTML = `
  766. <div class="s-table-wrapper">
  767. <table class="s-table forumbox">
  768. <thead>
  769. <tr class="block_txt_c0">
  770. <th class="c1" width="1">用户</th>
  771. <th class="c2">过滤规则</th>
  772. <th class="c3" width="1">特别关注</th>
  773. <th class="c4" width="1">操作</th>
  774. </tr>
  775. </thead>
  776. <tbody></tbody>
  777. </table>
  778. </div>
  779. <div class="silver" style="margin-top: 5px;">特别关注功能需要占用额外的资源,请谨慎开启</div>
  780. `;
  781.  
  782. return c;
  783. })();
  784.  
  785. let page = 0;
  786. let hasNext = false;
  787. let isFetching = false;
  788.  
  789. const box = content.querySelector("DIV");
  790.  
  791. const list = content.querySelector("TBODY");
  792.  
  793. const wrapper = content.querySelector(".s-table-wrapper");
  794.  
  795. const fetchData = () => {
  796. isFetching = true;
  797.  
  798. follow_list(page)
  799. .then((res) => {
  800. hasNext = Object.keys(res).length > 0;
  801.  
  802. for (let i in res) {
  803. const { uid, username } = res[i];
  804.  
  805. const data = extraData.getValue(uid) || {};
  806.  
  807. if (list.querySelector(`[data-id="${uid}"]`)) {
  808. continue;
  809. }
  810.  
  811. const item = document.createElement("TR");
  812.  
  813. item.className = `row${
  814. (list.querySelectorAll("TR").length % 2) + 1
  815. }`;
  816.  
  817. item.setAttribute("data-id", uid);
  818.  
  819. item.innerHTML = `
  820. <td class="c1">
  821. <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a>
  822. </td>
  823. <td class="c2">
  824. <div class="s-input-wrapper">
  825. <input value="${data.rule || ""}" />
  826. </div>
  827. </td>
  828. <td class="c3">
  829. <div style="text-align: center;">
  830. <input type="checkbox" ${
  831. data.level ? `checked="checked"` : ""
  832. } />
  833. </div>
  834. </td>
  835. <td class="c4">
  836. <div class="s-button-group">
  837. <button>重置</button>
  838. <button>移除</button>
  839. </div>
  840. </td>
  841. `;
  842.  
  843. const ruleElement = item.querySelector("INPUT");
  844. const levelElement = item.querySelector(`INPUT[type="checkbox"]`);
  845. const actions = item.querySelectorAll("BUTTON");
  846.  
  847. const save = () => {
  848. extraData.setValue(uid, {
  849. rule: ruleElement.value,
  850. level: levelElement.checked ? 1 : 0,
  851. });
  852. };
  853.  
  854. const clear = () => {
  855. ruleElement.value = "";
  856. levelElement.checked = false;
  857.  
  858. save();
  859. };
  860.  
  861. ruleElement.onchange = save;
  862.  
  863. levelElement.onchange = save;
  864.  
  865. actions[0].onclick = () => clear();
  866. actions[1].onclick = () => handleSwitchFollow(uid, 1);
  867.  
  868. list.appendChild(item);
  869. }
  870. })
  871. .finally(() => {
  872. isFetching = false;
  873. });
  874. };
  875.  
  876. box.onscroll = () => {
  877. if (isFetching || !hasNext) {
  878. return;
  879. }
  880.  
  881. if (
  882. box.scrollHeight - box.scrollTop - box.clientHeight <=
  883. wrapper.clientHeight
  884. ) {
  885. page = page + 1;
  886.  
  887. fetchData();
  888. }
  889. };
  890.  
  891. const refresh = () => {
  892. list.innerHTML = "";
  893.  
  894. page = 1;
  895. hasNext = false;
  896.  
  897. fetchData();
  898. };
  899.  
  900. hookFunction(u, "createView", (view) => {
  901. view.addModule({
  902. name,
  903. content,
  904. refresh,
  905. });
  906. });
  907. }
  908.  
  909. // 我的粉丝
  910. {
  911. const name = "我的粉丝";
  912.  
  913. const content = (() => {
  914. const c = document.createElement("div");
  915.  
  916. c.style.display = "none";
  917. c.innerHTML = `
  918. <div class="s-table-wrapper">
  919. <table class="s-table forumbox">
  920. <thead>
  921. <tr class="block_txt_c0">
  922. <th class="c1">用户</th>
  923. <th class="c2" width="1">操作</th>
  924. </tr>
  925. </thead>
  926. <tbody></tbody>
  927. </table>
  928. </div>
  929. `;
  930.  
  931. return c;
  932. })();
  933.  
  934. let page = 0;
  935. let hasNext = false;
  936. let isFetching = false;
  937.  
  938. const box = content.querySelector("DIV");
  939.  
  940. const list = content.querySelector("TBODY");
  941.  
  942. const wrapper = content.querySelector(".s-table-wrapper");
  943.  
  944. const fetchData = () => {
  945. isFetching = true;
  946.  
  947. follow_by_list(page)
  948. .then((res) => {
  949. hasNext = Object.keys(res).length > 0;
  950.  
  951. for (let i in res) {
  952. const { uid, username } = res[i];
  953.  
  954. if (list.querySelector(`[data-id="${uid}"]`)) {
  955. continue;
  956. }
  957.  
  958. const item = document.createElement("TR");
  959.  
  960. item.className = `row${
  961. (list.querySelectorAll("TR").length % 2) + 1
  962. }`;
  963.  
  964. item.setAttribute("data-id", uid);
  965.  
  966. item.innerHTML = `
  967. <td class="c1">
  968. <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a>
  969. </td>
  970. <td class="c2">
  971. <div class="s-button-group">
  972. <button>移除</button>
  973. </div>
  974. </td>
  975. `;
  976.  
  977. const action = item.querySelector("BUTTON");
  978.  
  979. action.onclick = () => handleRemoveFans(uid);
  980.  
  981. list.appendChild(item);
  982. }
  983. })
  984. .finally(() => {
  985. isFetching = false;
  986. });
  987. };
  988.  
  989. box.onscroll = () => {
  990. if (isFetching || !hasNext) {
  991. return;
  992. }
  993.  
  994. if (
  995. box.scrollHeight - box.scrollTop - box.clientHeight <=
  996. wrapper.clientHeight
  997. ) {
  998. page = page + 1;
  999.  
  1000. fetchData();
  1001. }
  1002. };
  1003.  
  1004. const refresh = () => {
  1005. list.innerHTML = "";
  1006.  
  1007. page = 1;
  1008. hasNext = false;
  1009.  
  1010. fetchData();
  1011. };
  1012.  
  1013. hookFunction(u, "createView", (view) => {
  1014. view.addModule({
  1015. name,
  1016. content,
  1017. refresh,
  1018. });
  1019. });
  1020. }
  1021.  
  1022. // 关注动态
  1023. {
  1024. const name = "关注动态";
  1025.  
  1026. const content = (() => {
  1027. const c = document.createElement("div");
  1028.  
  1029. c.style.display = "none";
  1030. c.innerHTML = `
  1031. <div class="s-table-wrapper">
  1032. <table class="s-table forumbox">
  1033. <thead>
  1034. <tr class="block_txt_c0">
  1035. <th class="c1" width="1">时间</th>
  1036. <th class="c2">内容</th>
  1037. </tr>
  1038. </thead>
  1039. <tbody></tbody>
  1040. </table>
  1041. </div>
  1042. `;
  1043.  
  1044. return c;
  1045. })();
  1046.  
  1047. let page = 0;
  1048. let hasNext = false;
  1049. let isFetching = false;
  1050.  
  1051. const box = content.querySelector("DIV");
  1052.  
  1053. const list = content.querySelector("TBODY");
  1054.  
  1055. const wrapper = content.querySelector(".s-table-wrapper");
  1056.  
  1057. const fetchData = () => {
  1058. isFetching = true;
  1059.  
  1060. follow_dymanic_list(page)
  1061. .then((res) =>
  1062. Promise.all(
  1063. Object.keys(res[1]).map((uid) =>
  1064. get_user_info(uid).then((item) => {
  1065. if (item.follow) {
  1066. const info = extraData.getValue(uid) || {
  1067. rule: "",
  1068. level: 0,
  1069. };
  1070.  
  1071. extraData.setValue(uid, {
  1072. ...info,
  1073. });
  1074. } else {
  1075. extraData.remove(uid);
  1076. }
  1077. })
  1078. )
  1079. ).then(() => {
  1080. return res;
  1081. })
  1082. )
  1083. .then((res) => {
  1084. hasNext = res[2] > res[3];
  1085.  
  1086. extraData.setValue(0, {
  1087. time: Math.floor(new Date() / 1000),
  1088. unread: 0,
  1089. });
  1090.  
  1091. return res;
  1092. })
  1093. .then((res) => {
  1094. const filtered = Object.values(res[0])
  1095. .map((item) => ({
  1096. id: item[0],
  1097. uid: item[2],
  1098. info: item[4]
  1099. ? res[4][`${item[3]}_${item[4]}`]
  1100. : res[4][item[3]],
  1101. time: item[6],
  1102. summary: item.summary
  1103. .replace(
  1104. /\[uid=(\d+)\](.+)\[\/uid\]/,
  1105. `<a href="/nuke.php?func=ucp&uid=${item[2]}" class="b nobr">$2</a>`
  1106. )
  1107. .replace(
  1108. /\[pid=(\d+)\](.+)\[\/pid\](\s?)/,
  1109. `<a href="/read.php?pid=${item[4]}" class="b nobr">回复</a>`
  1110. )
  1111. .replace(
  1112. /\[tid=(\d+)\](.+)\[\/tid\]/,
  1113. item[4] === 0
  1114. ? `<a href="/read.php?tid=${item[3]}" title="$2" class="b nobr">$2</a>`
  1115. : `<a href="/read.php?pid=${item[4]}&opt=128" title="$2" class="b nobr">$2</a>`
  1116. ),
  1117. }))
  1118. .filter((item) => {
  1119. const { uid, info } = item;
  1120.  
  1121. const data = extraData.getValue(uid);
  1122.  
  1123. if (data) {
  1124. const { rule } = data;
  1125.  
  1126. if (rule) {
  1127. return (
  1128. info.subject.search(rule) >= 0 ||
  1129. info.content.search(rule) >= 0
  1130. );
  1131. }
  1132.  
  1133. return true;
  1134. }
  1135.  
  1136. return false;
  1137. });
  1138.  
  1139. return filtered;
  1140. })
  1141. .then((res) => {
  1142. for (let i in res) {
  1143. const { id, time, summary } = res[i];
  1144.  
  1145. if (list.querySelector(`[data-id="${id}"]`)) {
  1146. continue;
  1147. }
  1148.  
  1149. const item = document.createElement("TR");
  1150.  
  1151. item.className = `row${
  1152. (list.querySelectorAll("TR").length % 2) + 1
  1153. }`;
  1154.  
  1155. item.setAttribute("data-id", id);
  1156. item.setAttribute("data-time", time);
  1157.  
  1158. item.innerHTML = `
  1159. <td class="c1">
  1160. <span class="nobr">${ui.time2dis(time)}</span>
  1161. </td>
  1162. <td class="c2">
  1163. <div class="s-text-ellipsis">
  1164. <span>${summary}</span>
  1165. </div>
  1166. </td>
  1167. `;
  1168.  
  1169. list.appendChild(item);
  1170. }
  1171.  
  1172. if (box.scrollHeight === box.clientHeight && hasNext) {
  1173. page = page + 1;
  1174.  
  1175. fetchData();
  1176. }
  1177. })
  1178. .finally(() => {
  1179. isFetching = false;
  1180. });
  1181. };
  1182.  
  1183. box.onscroll = () => {
  1184. if (isFetching || !hasNext) {
  1185. return;
  1186. }
  1187.  
  1188. if (
  1189. box.scrollHeight - box.scrollTop - box.clientHeight <=
  1190. wrapper.clientHeight
  1191. ) {
  1192. page = page + 1;
  1193.  
  1194. fetchData();
  1195. }
  1196. };
  1197.  
  1198. const refresh = () => {
  1199. list.innerHTML = "";
  1200.  
  1201. page = 1;
  1202. hasNext = false;
  1203.  
  1204. fetchData();
  1205. };
  1206.  
  1207. hookFunction(u, "createView", (view) => {
  1208. view.addModule({
  1209. name,
  1210. content,
  1211. refresh,
  1212. });
  1213. });
  1214. }
  1215.  
  1216. // 打开菜单
  1217. const handleCreateView = (() => {
  1218. let view, window;
  1219.  
  1220. return () => {
  1221. if (view === undefined) {
  1222. view = u.createView();
  1223. }
  1224.  
  1225. u.modules["关注动态"].toggle();
  1226. m.update(0);
  1227.  
  1228. if (window === undefined) {
  1229. window = ui.createCommmonWindow();
  1230. }
  1231.  
  1232. window._.addContent(null);
  1233. window._.addTitle(`关注`);
  1234. window._.addContent(view.content);
  1235. window._.show();
  1236. };
  1237. })();
  1238.  
  1239. // 扩展用户信息
  1240. (() => {
  1241. const execute = (argid) => {
  1242. const args = ui.postArg.data[argid];
  1243.  
  1244. if (args.comment) return;
  1245.  
  1246. const uid = +args.pAid;
  1247.  
  1248. if (uid > 0) {
  1249. if (info[uid] === undefined) {
  1250. info[uid] = new UserInfo(uid);
  1251. }
  1252.  
  1253. if (document.contains(info[uid].container[argid]) === false) {
  1254. info[uid].container[argid] =
  1255. args.uInfoC.closest("tr").querySelector(".posterInfoLine") ||
  1256. args.uInfoC.querySelector("div");
  1257. }
  1258.  
  1259. info[uid].enqueue(async () => {
  1260. args.uInfoC.className =
  1261. args.uInfoC.className + " s-user-info-container";
  1262.  
  1263. if (info[uid].children[16]) {
  1264. info[uid].children[16].destroy();
  1265. }
  1266.  
  1267. info[uid].children[16] = new UserInfoWidget(
  1268. (data) => {
  1269. const value = data.follow_by_num || 0;
  1270.  
  1271. const element = document.createElement("SPAN");
  1272.  
  1273. if (uid === self || data.follow) {
  1274. element.className =
  1275. "small_colored_text_btn stxt block_txt_c2 vertmod";
  1276. } else {
  1277. element.className =
  1278. "small_colored_text_btn stxt block_txt_c2 vertmod ah";
  1279. }
  1280.  
  1281. element.style.cursor = "default";
  1282. element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>&nbsp;${value}</span>`;
  1283.  
  1284. element.style.cursor = "pointer";
  1285.  
  1286. return element;
  1287. },
  1288. (data, element) => {
  1289. element.onclick = () => {
  1290. if (data.uid === self) {
  1291. handleCreateView();
  1292. } else {
  1293. handleSwitchFollow(data.uid, data.follow);
  1294. }
  1295. };
  1296. }
  1297. );
  1298.  
  1299. info[uid].rearrange();
  1300. });
  1301. }
  1302. };
  1303.  
  1304. let initialized = false;
  1305.  
  1306. if (ui.postArg) {
  1307. Object.keys(ui.postArg.data).forEach((i) => execute(i));
  1308. }
  1309.  
  1310. hookFunction(ui, "eval", () => {
  1311. if (initialized) return;
  1312.  
  1313. if (ui.postDisp) {
  1314. hookFunction(
  1315. ui,
  1316. "postDisp",
  1317. (returnValue, originalFunction, arguments) => execute(arguments[0])
  1318. );
  1319.  
  1320. initialized = true;
  1321. }
  1322. });
  1323. })();
  1324.  
  1325. // 提醒关注
  1326. (async () => {
  1327. // 增加菜单项
  1328. m.create(handleCreateView);
  1329.  
  1330. // 获取动态
  1331. (() => {
  1332. const cache = extraData.getValue(0) || {
  1333. time: 0,
  1334. unread: 0,
  1335. };
  1336.  
  1337. const fetchData = async (page = 1, result = {}) =>
  1338. new Promise((resolve) => {
  1339. follow_dymanic_list(page).then(async (res) => {
  1340. const list = Object.values(res[0]);
  1341. const prefiltered = list
  1342. .map((item) => ({
  1343. id: item[0],
  1344. uid: item[2],
  1345. info: item[4]
  1346. ? res[4][`${item[3]}_${item[4]}`]
  1347. : res[4][item[3]],
  1348. time: item[6],
  1349. }))
  1350. .filter((item) => item.time > (cache.time || 0))
  1351. .filter((item) => {
  1352. if (result[item.id]) {
  1353. return false;
  1354. }
  1355.  
  1356. result[item.id] = item;
  1357. return true;
  1358. });
  1359.  
  1360. if (prefiltered.length) {
  1361. await Promise.all(
  1362. Object.keys(res[1]).map((uid) =>
  1363. get_user_info(uid).then((item) => {
  1364. if (item.follow) {
  1365. const info = extraData.getValue(uid) || {
  1366. rule: "",
  1367. level: 0,
  1368. };
  1369.  
  1370. extraData.setValue(uid, {
  1371. ...info,
  1372. });
  1373. } else {
  1374. extraData.remove(uid);
  1375. }
  1376. })
  1377. )
  1378. );
  1379.  
  1380. const hasNext =
  1381. prefiltered.length === list.length && res[2] > res[3];
  1382.  
  1383. if (hasNext) {
  1384. const withNext = await fetchData(page + 1, result);
  1385.  
  1386. resolve(withNext);
  1387. }
  1388. }
  1389.  
  1390. resolve(result);
  1391. });
  1392. });
  1393.  
  1394. fetchData().then((res) => {
  1395. const filtered = Object.values(res).filter((item) => {
  1396. const { uid, info } = item;
  1397.  
  1398. const data = extraData.getValue(uid);
  1399.  
  1400. if (data) {
  1401. const { rule } = data;
  1402.  
  1403. if (rule) {
  1404. return (
  1405. info.subject.search(rule) >= 0 ||
  1406. info.content.search(rule) >= 0
  1407. );
  1408. }
  1409.  
  1410. return true;
  1411. }
  1412.  
  1413. return false;
  1414. });
  1415.  
  1416. const unread = (cache.unread || 0) + filtered.length;
  1417.  
  1418. extraData.setValue(0, {
  1419. time: Math.floor(new Date() / 1000),
  1420. unread: unread,
  1421. });
  1422.  
  1423. m.update(unread);
  1424. });
  1425. })();
  1426.  
  1427. // 特别关注
  1428. {
  1429. const fetchData = async (uid, value) => {
  1430. // 请求用户信息
  1431. const { username, follow, posts } = await get_user_info(uid);
  1432.  
  1433. // 用户缓存
  1434. const { rule, time, postNum } = value;
  1435.  
  1436. // 已取消关注
  1437. if (follow === 0) {
  1438. extraData.remove(uid);
  1439. return [];
  1440. }
  1441.  
  1442. // 判断是否有新活动
  1443. if (posts <= (postNum || 0)) {
  1444. return [];
  1445. }
  1446.  
  1447. // 是否匹配
  1448. const isMatch = (text) => {
  1449. if (rule) {
  1450. return text.search(rule) >= 0;
  1451. }
  1452.  
  1453. return true;
  1454. };
  1455.  
  1456. // 请求发帖记录
  1457. const ts = await get_user_topic_list(uid).then((res) =>
  1458. Object.values(res)
  1459. .filter(
  1460. (item) => item.postdate > (time || 0) && isMatch(item.subject)
  1461. )
  1462. .map((item) => ({
  1463. 0: 5,
  1464. 1: item.authorid,
  1465. 2: item.author,
  1466. 5: item.subject,
  1467. 6: item.tid,
  1468. 9: item.postdate,
  1469. 10: 1,
  1470. }))
  1471. );
  1472.  
  1473. // 请求回帖记录
  1474. const ps = await get_user_post_list(uid).then((res) =>
  1475. Object.values(res)
  1476. .filter(
  1477. (item) =>
  1478. item.__P.postdate > (time || 0) && isMatch(item.__P.content)
  1479. )
  1480. .map((item) => ({
  1481. 0: 6,
  1482. 1: uid,
  1483. 2: username,
  1484. 5: item.subject,
  1485. 6: item.__P.tid,
  1486. 7: item.__P.pid,
  1487. 9: item.__P.postdate,
  1488. 10: 1,
  1489. }))
  1490. );
  1491.  
  1492. // 更新缓存
  1493. extraData.setValue(uid, {
  1494. ...value,
  1495. time: Math.floor(new Date() / 1000),
  1496. postNum: posts,
  1497. });
  1498.  
  1499. // 返回结果
  1500. return [...ts, ...ps];
  1501. };
  1502.  
  1503. const data = (
  1504. await Promise.all(
  1505. extraData
  1506. .specialList()
  1507. .map(async ([key, value]) => await fetchData(key, value))
  1508. )
  1509. )
  1510. .flat()
  1511. .sort((a, b) => a[9] - b[9]);
  1512.  
  1513. if (Object.keys(data).length) {
  1514. const func = () => {
  1515. // 修复 NGA 脚本错误
  1516. TPL[KEY["_BIT_SYS"]][KEY["_TYPE_KEYWORD_WATCH_REPLY"]] = function (
  1517. x
  1518. ) {
  1519. return x[KEY["_ABOUT_ID_4"]]
  1520. ? "{_U} 在{_T1} {_R2} 中的 {_R5} 触发了关键词监视<br/>"
  1521. : "{_U} 在主题 {_T} 中的 {_R5} 触发了关键词监视<br/>";
  1522. };
  1523.  
  1524. // 推送消息
  1525. for (let i in data) {
  1526. ui.notification._add(1, data[i], 1);
  1527. }
  1528.  
  1529. // 打开窗口
  1530. ui.notification.openBox();
  1531. };
  1532.  
  1533. if (ui.notification) {
  1534. func();
  1535. } else {
  1536. ui.loadNotiScript(() => {
  1537. func();
  1538. });
  1539. }
  1540. }
  1541. }
  1542. })();
  1543. })(ui.sn.userInfo);
  1544. })(commonui, __CURRENT_UID);

QingJ © 2025

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