您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
同步客户端关注功能
当前为
// ==UserScript== // @name NGA Watcher // @namespace https://gf.qytechs.cn/users/263018 // @version 1.2.1 // @author snyssss // @description 同步客户端关注功能 // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @noframes // ==/UserScript== ((ui, self) => { if (!ui) return; // 钩子 const hookFunction = (object, functionName, callback) => { ((originalFunction) => { object[functionName] = function () { const returnValue = originalFunction.apply(this, arguments); callback.apply(this, [returnValue, originalFunction, arguments]); return returnValue; }; })(object[functionName]); }; // 用户信息 class UserInfo { execute(task) { task().finally(() => { if (this.waitingQueue.length) { const next = this.waitingQueue.shift(); this.execute(next); } else { this.isRunning = false; } }); } enqueue(task) { if (this.isRunning) { this.waitingQueue.push(task); } else { this.isRunning = true; this.execute(task); } } rearrange() { if (this.data) { const list = Object.values(this.children); for (let i = 0; i < list.length; i++) { if (list[i].source === undefined) { list[i].create(this.data); } Object.entries(this.container).forEach((item) => { list[i].clone(this.data, item); }); } } } reload() { this.enqueue(async () => { this.data = await new Promise((resolve) => { fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${this.uid}`) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); resolve(result.data[0]); }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve(); }); }); Object.values(this.children).forEach((item) => item.destroy()); this.rearrange(); }); } constructor(id) { this.uid = id; this.waitingQueue = []; this.isRunning = false; this.container = {}; this.children = {}; this.reload(); } } // 用户信息组件 class UserInfoWidget { destroy() { if (this.source) { this.source = undefined; } if (this.target) { Object.values(this.target).forEach((item) => { if (item.parentNode) { item.parentNode.removeChild(item); } }); } } clone(data, [argid, container]) { if (this.source) { if (this.target[argid] === undefined) { this.target[argid] = this.source.cloneNode(true); if (this.callback) { this.callback(data, this.target[argid]); } } container.appendChild(this.target[argid]); } } constructor(func, callback) { this.create = (data) => { this.destroy(); this.source = func(data); this.target = {}; }; this.callback = callback; } } ui.sn = ui.sn || {}; ui.sn.userInfo = ui.sn.userInfo || {}; ((info) => { // STYLE GM_addStyle(` .s-user-info-container:not(:hover) .ah { display: none !important; } .s-table { border: 1px solid #ead5bc; border-left: none; border-bottom: none; width: 99.95%; } .s-table thead { background-color: #591804; color: #fff8e7; } .s-table tbody tr { background-color: #fff0cd; } .s-table tbody tr:nth-of-type(odd) { background-color: #fff8e7; } .s-table td { border: 1px solid #ead5bc; border-top: none; border-right: none; padding: 6px; white-space: nowrap; } .s-table input:not([type]), .s-table input[type="text"] { margin: 0; width: 100%; box-sizing: border-box; } .s-button-group { margin: -.1em -.2em; } `); // UI const u = (() => { const modules = {}; const createView = () => { const tabContainer = (() => { const c = document.createElement("div"); c.className = "w100"; c.innerHTML = ` <div class="right_" style="margin-bottom: 5px;"> <table class="stdbtn" cellspacing="0"> <tbody> <tr></tr> </tbody> </table> </div> <div class="clear"></div> `; return c; })(); const tabPanelContainer = (() => { const c = document.createElement("div"); c.style = "width: 800px;"; return c; })(); const content = (() => { const c = document.createElement("div"); c.append(tabContainer); c.append(tabPanelContainer); return c; })(); const addModule = (() => { const tc = tabContainer.getElementsByTagName("tr")[0]; const cc = tabPanelContainer; return (module) => { const tabBox = document.createElement("td"); tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`; const tab = tabBox.childNodes[0]; const toggle = () => { Object.values(modules).forEach((item) => { if (item.tab === tab) { item.tab.className = "nobr"; item.content.style = "display: block"; item.visible = true; } else { item.tab.className = "nobr silver"; item.content.style = "display: none"; item.visible = false; } }); module.refresh(); }; tc.append(tabBox); cc.append(module.content); tab.onclick = toggle; modules[module.name] = { ...module, tab, toggle, visible: false, }; return modules[module.name]; }; })(); return { content, modules, addModule, }; }; const refresh = () => { Object.values(modules) .find((item) => item.visible) ?.refresh(); }; return { createView, refresh, }; })(); // 扩展规则 const extraData = (() => { const key = `EXTRA_DATA`; const data = GM_getValue(key) || {}; const save = () => { GM_setValue(key, data); }; const setValue = (uid, value) => { data[uid] = value; save(); }; const getValue = (uid) => data[uid]; const remove = (uid) => { delete data[uid]; save(); }; const specialList = () => { const result = Object.entries(data).filter( ([key, value]) => value.level ); if (Object.keys(result)) { return result; } return null; }; GM_addValueChangeListener(key, function (_, prev, next) { Object.assign(data, next); }); return { specialList, setValue, getValue, remove, }; })(); // 获取用户信息 const get_user_info = (uid) => new Promise((resolve, reject) => { fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 获取用户发帖列表 const get_user_topic_list = (uid) => new Promise((resolve) => { fetch(`/thread.php?lite=js&authorid=${uid}`) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data.__T); } else { resolve({}); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve({}); }); }); // 获取用户回帖列表 const get_user_post_list = (uid) => new Promise((resolve, reject) => { fetch(`/thread.php?lite=js&authorid=${uid}&searchpost=1`) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data.__T); } else { resolve({}); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve({}); }); }); // 关注 const follow = (uid) => new Promise((resolve, reject) => { fetch( `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=1`, { method: "post", } ) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 取消关注 const un_follow = (uid) => new Promise((resolve, reject) => { fetch( `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=8`, { method: "post", } ) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 移除粉丝 const un_follow_fans = (uid) => new Promise((resolve, reject) => { fetch( `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=256`, { method: "post", } ) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 获取关注列表 const follow_list = (page) => new Promise((resolve, reject) => { fetch( `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow&page=${page}`, { method: "post", } ) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 获取粉丝列表 const follow_by_list = (page) => new Promise((resolve, reject) => { fetch( `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow_by&page=${page}`, { method: "post", } ) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data[0]); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 获取关注动态 const follow_dymanic_list = () => new Promise((resolve, reject) => { fetch(`/nuke.php?lite=js&__lib=follow_v2&__act=get_push_list`, { method: "post", }) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data); } else { reject(result.error[0]); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject(); }); }); // 切换关注 const handleSwitchFollow = (uid, isFollow) => { if (isFollow) { if (confirm("取消关注?")) { un_follow(uid).then(() => { info[uid]?.reload(); u.refresh(); }); } } else { follow(uid).then(() => { info[uid]?.reload(); u.refresh(); }); } }; // 移除粉丝 const handleRemoveFans = (uid) => { if (confirm("移除粉丝?")) { un_follow_fans(uid).then(() => { u.refresh(); }); } }; // 打开菜单 const handleCreateView = (() => { let view, window; return () => { if (view === undefined) { view = u.createView(); } view.modules["关注动态"].toggle(); if (window === undefined) { window = ui.createCommmonWindow(); } window._.addContent(null); window._.addTitle(`关注`); window._.addContent(view.content); window._.show(); }; })(); // 我的关注 { const name = "我的关注"; const height = 39 + 38 * 10; const content = (() => { const c = document.createElement("div"); c.style.display = "none"; c.innerHTML = ` <div style="height: ${height}px; overflow: auto;"> <table class="s-table"> <thead> <tr> <td width="1">用户</td> <td>过滤规则</td> <td width="1">特别关注</td> <td width="1">操作</td> </tr> </thead> <tbody></tbody> </table> </div> <span class="silver">特别关注功能需要占用额外的资源,请谨慎开启</span> `; return c; })(); let page = 0; let lastSize = -1; let isFetching = false; const box = content.querySelector("DIV"); const list = content.querySelector("TBODY"); const fetchData = () => { isFetching = true; follow_list(page) .then((res) => { lastSize = Object.keys(res).length; for (let i in res) { const { uid, username } = res[i]; const data = extraData.getValue(uid) || {}; if (list.querySelector(`[data-id="${uid}"]`)) { continue; } const item = document.createElement("TR"); item.setAttribute("data-id", uid); item.innerHTML = ` <td> <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a> </td> <td> <input value="${data.rule || ""}" /> </td> <td> <div style="text-align: center;"> <input type="checkbox" ${ data.level ? `checked="checked"` : "" } /> </div> </td> <td> <div class="s-button-group"> <button>重置</button> <button>移除</button> </div> </td> `; const ruleElement = item.querySelector("INPUT"); const levelElement = item.querySelector(`INPUT[type="checkbox"]`); const actions = item.querySelectorAll("BUTTON"); const save = () => { extraData.setValue(uid, { rule: ruleElement.value, level: levelElement.checked ? 1 : 0, }); }; const clear = () => { ruleElement.value = ""; levelElement.checked = false; save(); }; ruleElement.onchange = save; levelElement.onchange = save; actions[0].onclick = () => clear(); actions[1].onclick = () => handleSwitchFollow(uid, 1); list.appendChild(item); } }) .finally(() => { isFetching = false; }); }; box.onscroll = () => { if (isFetching || lastSize === 0) { return; } if (box.scrollHeight - box.scrollTop - box.clientHeight <= height) { page = page + 1; fetchData(); } }; const refresh = () => { list.innerHTML = ""; page = 1; lastSize = -1; fetchData(); }; hookFunction(u, "createView", (view) => { view.addModule({ name, content, refresh, }); }); } // 我的粉丝 { const name = "我的粉丝"; const height = 39 + 38 * 10; const content = (() => { const c = document.createElement("div"); c.style.display = "none"; c.innerHTML = ` <div style="height: ${height}px; overflow: auto;"> <table class="s-table"> <thead> <tr> <td>用户</td> <td width="1">操作</td> </tr> </thead> <tbody></tbody> </table> </div> `; return c; })(); let page = 0; let lastSize = -1; let isFetching = false; const box = content.querySelector("DIV"); const list = content.querySelector("TBODY"); const fetchData = () => { isFetching = true; follow_by_list(page) .then((res) => { lastSize = Object.keys(res).length; for (let i in res) { const { uid, username } = res[i]; if (list.querySelector(`[data-id="${uid}"]`)) { continue; } const item = document.createElement("TR"); item.setAttribute("data-id", uid); item.innerHTML = ` <td> <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a> </td> <td> <div class="s-button-group"> <button>移除</button> </div> </td> `; const action = item.querySelector("BUTTON"); action.onclick = () => handleRemoveFans(uid); list.appendChild(item); } }) .finally(() => { isFetching = false; }); }; box.onscroll = () => { if (isFetching || lastSize === 0) { return; } if (box.scrollHeight - box.scrollTop - box.clientHeight <= height) { page = page + 1; fetchData(); } }; const refresh = () => { list.innerHTML = ""; page = 1; lastSize = -1; fetchData(); }; hookFunction(u, "createView", (view) => { view.addModule({ name, content, refresh, }); }); } // 关注动态 { const name = "关注动态"; const height = 39 + 38 * 10; const content = (() => { const c = document.createElement("div"); c.style.display = "none"; c.innerHTML = ` <div style="height: ${height}px; overflow: auto;"> <table class="s-table"> <thead> <tr> <td width="1">时间</td> <td>内容</td> </tr> </thead> <tbody></tbody> </table> </div> `; return c; })(); let page = 0; let lastSize = -1; let isFetching = false; const box = content.querySelector("DIV"); const list = content.querySelector("TBODY"); const fetchData = () => { isFetching = true; follow_dymanic_list(page) .then((res) => { if (res[2] === res[3]) { lastSize = 0; } else { lastSize = -1; } const filtered = Object.values(res[0]) .map((item) => ({ id: item[0], uid: item[2], tid: item[3], pid: item[4], time: item[6], summary: item.summary .replace( /\[uid=(\d+)\](.+)\[\/uid\]/, `<a href="/nuke.php?func=ucp&uid=$1" class="b nobr">$2</a>` ) .replace( /\[pid=(\d+)\](.+)\[\/pid\]/, `<a href="/read.php?pid=$1" class="b nobr">回复</a>` ) .replace(/\[tid=(\d+)\](.+)\[\/tid\]/, function ($0, $1, $2) { let s = ui.cutstrbylen($2, 19); if (s.length < $2.length) { s += "..."; } return `<a href="/read.php?tid=${$1}" class="b nobr">${s}</a>`; }), })) .filter((item) => { const data = extraData.getValue(item.uid); if (data) { const { rule } = data; if (rule) { const info = res[4][item.pid ? `${item.tid}_${item.pid}` : item.tid]; return ( info.subject.search(rule) >= 0 || info.content.search(rule) >= 0 ); } } return true; }); return filtered; }) .then((res) => { for (let i in res) { const { id, time, summary } = res[i]; if (list.querySelector(`[data-id="${id}"]`)) { continue; } const item = document.createElement("TR"); item.setAttribute("data-id", id); item.setAttribute("data-time", time); item.innerHTML = ` <td> <span style="white-space: nowrap;">${ui.time2dis(time)}</span> </td> <td> ${summary} </td> `; list.appendChild(item); } }) .finally(() => { isFetching = false; }); }; box.onscroll = () => { if (isFetching || lastSize === 0) { return; } if (box.scrollHeight - box.scrollTop - box.clientHeight <= height) { page = page + 1; fetchData(); } }; const refresh = () => { list.innerHTML = ""; page = 1; lastSize = -1; fetchData(); }; hookFunction(u, "createView", (view) => { view.addModule({ name, content, refresh, }); }); } // 增加菜单项 if (document.querySelector(`[name="unisearchinput"]`)) { const anchor = document.querySelector("#mainmenu .td:last-child"); const button = document.createElement("DIV"); button.className = `td`; button.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">关注</a>`; button.onclick = () => handleCreateView(); anchor.before(button); } const execute = (argid) => { const args = ui.postArg.data[argid]; if (args.comment) return; const uid = +args.pAid; if (uid > 0) { if (info[uid] === undefined) { info[uid] = new UserInfo(uid); } if (document.contains(info[uid].container[argid]) === false) { info[uid].container[argid] = args.uInfoC.querySelector( "[name=uid]" ).parentNode; } info[uid].enqueue(async () => { args.uInfoC.className = args.uInfoC.className + " s-user-info-container"; if (info[uid].children[16]) { info[uid].children[16].destroy(); } info[uid].children[16] = new UserInfoWidget( (data) => { const value = data.follow_by_num || 0; const element = document.createElement("SPAN"); if (uid === self || data.follow) { element.className = "small_colored_text_btn stxt block_txt_c2 vertmod"; } else { element.className = "small_colored_text_btn stxt block_txt_c2 vertmod ah"; } element.style.cursor = "default"; element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span> ${value}</span>`; element.style.cursor = "pointer"; return element; }, (data, element) => { element.onclick = () => { if (data.uid === self) { handleCreateView(); } else { handleSwitchFollow(data.uid, data.follow); } }; } ); info[uid].rearrange(); }); } }; if (self) { let initialized = false; if (ui.postArg) { Object.keys(ui.postArg.data).forEach((i) => execute(i)); } hookFunction(ui, "eval", () => { if (initialized) return; if (ui.postDisp) { hookFunction( ui, "postDisp", (returnValue, originalFunction, arguments) => execute(arguments[0]) ); initialized = true; } }); } // 特别关注 (async () => { const fetchData = async (uid, value) => { // 请求用户信息 const { username, follow, posts } = await get_user_info(uid); // 用户缓存 const { rule, time, postNum } = value; // 已取消关注 if (follow === 0) { extraData.remove(uid); return []; } // 判断是否有新活动 if (posts <= (postNum || 0)) { return []; } // 是否匹配 const isMatch = function (text) { if (rule) { return text.search(rule) >= 0; } return true; }; // 请求发帖记录 const ts = await get_user_topic_list(uid).then((res) => Object.values(res) .filter( (item) => item.postdate > (time || 0) && isMatch(item.subject) ) .map((item) => ({ 0: 5, 1: item.authorid, 2: item.author, 5: item.subject, 6: item.tid, 9: item.postdate, 10: 1, })) ); // 请求回帖记录 const rs = await get_user_post_list(uid).then((res) => Object.values(res) .filter( (item) => item.__P.postdate > (time || 0) && isMatch(item.__P.content) ) .map((item) => ({ 0: 6, 1: uid, 2: username, 5: item.subject, 6: item.__P.tid, 7: item.__P.pid, 9: item.__P.postdate, 10: 1, })) ); // 更新缓存 extraData.setValue(uid, { ...value, time: parseInt(new Date() / 1000, 10), postNum: posts, }); // 返回结果 return [...ts, ...rs]; }; const data = ( await Promise.all( extraData .specialList() .map(async ([key, value]) => await fetchData(key, value)) ) ) .flat() .sort((a, b) => a[9] - b[9]); if (Object.keys(data).length) { const func = () => { // 修复 NGA 脚本错误 TPL[KEY["_BIT_SYS"]][KEY["_TYPE_KEYWORD_WATCH_REPLY"]] = function ( x ) { return x[KEY["_ABOUT_ID_4"]] ? "{_U} 在{_T1} {_R2} 中的 {_R5} 触发了关键词监视<br/>" : "{_U} 在主题 {_T} 中的 {_R5} 触发了关键词监视<br/>"; }; // 推送消息 for (let i in data) { ui.notification._add(1, data[i], 1); } // 打开窗口 ui.notification.openBox(); }; if (ui.notification) { func(); } else { ui.loadNotiScript(() => { func(); }); } } })(); })(ui.sn.userInfo); })(commonui, __CURRENT_UID);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址