// ==UserScript== // @name North-Plus Notification // @namespace https://github.com/sssssssl/NP-scripts // @version 0.1 // @description 查看自己发的主题的回复 // @author sl // @match https://*.white-plus.net/* // @match https://*.south-plus.net/* // @match https://*.imoutolove.me/* // @require https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js // @grant GM_getValue // @grant GM_setValue // @downloadURL https://update.greasyfork.icu/scripts/369603/North-Plus%20Notification.user.js // @updateURL https://update.greasyfork.icu/scripts/369603/North-Plus%20Notification.meta.js // ==/UserScript== (function () { 'use strict'; // 1. CONSTANTS var lastPullTimeKey = 'lastPullTime'; var unACKTimeKey = 'lastUnACKTime'; var unACKDataKey = 'unACKData'; var mappingKey = 'tidCommentMap'; var uidKey = 'uid'; // this needs to be tested. var pullInterval = 1000 * 60 * 2; var maxUnACKAge = 1000 * 60 * 1; var checkInterval = 1000 * 5 * 1; var maxRetry = 5; // 2. GLOBAL STATE var logLevels = { verbose: 1, silent: 2 }; var dev = false; var g = { url: window.location.href, stopNofitication: false, unInitialized: true, stopLoop: false, retry: 0, has_mypost_btn: false, checkInterval: checkInterval, pullInterval: pullInterval, logLevel : logLevels.verbose }; var originalTitle = document.title; // 3. CODE ENTRYPOINT var lastPullTime = GM_getValue(lastPullTimeKey); // 第一次在用户机器上运行 if (!lastPullTime) { getPostStatus(true); } grabUserInfo(); addStopBlinkCallback(); addSelfCommentCallback(); mainLoop(); function mainLoop() { app(); setTimeout(function () { if (!g.stopLoop) { mainLoop(); } }, g.checkInterval); } function app() { var unACKData = GM_getValue(unACKDataKey); if (unACKData) { log(['存在未合并数据', unACKData]); g.stopNotification = false; sendNotification(unACKData); // var lastUnACKTime = GM_getValue(unACKTimeKey); // var now = Date.now() // // 当未确认数据寿命达到允许的最大值,进行合并 // if (lastUnACKTime && (now - lastUnACKTime) > maxUnACKAge) { // updateMapping(unACKData); // } } getPostStatus(false); } function getPostStatus(first = false) { function timeCheck() { var now = Date.now(); var lastPullTime = GM_getValue(lastPullTimeKey); if (lastPullTime) { var dtime = (now - lastPullTime); log('距离上次拉取评论 ' + (dtime / 1000) + ' 秒'); } return !(lastPullTime && dtime < g.pullInterval); } var needUpdate = timeCheck(); if (!needUpdate) { g.checkInterval = g.checkInterval + 1000 * 5; g.checkInterval = Math.min(g.checkInterval, 1000 * 20); g.pullInterval = g.pullInterval + 1000 * 60 * 1; if (g.pullInterval > 1000 * 60 * 4) { g.pullInterval = pullInterval; } return; } else { g.checkInterval = checkInterval; g.pullInterval = pullInterval; } if (needUpdate) { var myPostURL = getPostURL(dev); $.ajax({ url: myPostURL, type: 'GET', success: function (data) { // 调用返回后才更新 lastPullTime, 增加访问服务器的时间间隔 GM_setValue(lastPullTimeKey, Date.now()); var newMap = getParser(dev)(data); var mapping = getMapping(); var diffInfo = compareMap(mapping, newMap); if (diffInfo.nNewComment > 0) { if (first) { log('首次执行.'); updateMapping(diffInfo.diffMap); } else { log(['写入 unACKData:', diffInfo]); GM_setValue(unACKDataKey, diffInfo); GM_setValue(unACKTimeKey, Date.now()); sendNotification(diffInfo); } } else { log('评论数量没有变化.'); } }, error: function (data) { g.retry += 1; if (g.retry == maxRetry) { g.stopLoop = true; console.log('网络似乎有问题,停止拉取评论'); } console.log([data]); } }); } } // 4. DATA PROCESSING function getPostURL(lv) { if (lv) { return 'posts'; } return 'u.php?action-topic.html'; } function getParser(lv) { if (lv) { return JSON.parse; } return parseData; } // 比较拉取到的评论数与本地保存的评论数的区别 function parseData(data) { var dataMap = {}; var pat = /read.php\?tid-(\d+).html.+?回复:(\d+)/g; var m; while (m = pat.exec(data)) { var tid = m[1]; var num = parseInt(m[2]); dataMap[tid] = num; } return dataMap; } function compareMap(baseMap, newMap) { var diffInfo = { nNewComment: 0, diffMap: {}, }; for (var key in newMap) { diffInfo.diffMap[key] = newMap[key]; if (!(key in baseMap)) { diffInfo.nNewComment += newMap[key]; } else { diffInfo.nNewComment += newMap[key] - baseMap[key]; } } return diffInfo; // debug if (diffInfo.nNewComment > 0) { log([ 'from compareMapp:', '有 ' + diffInfo.nNewComment + ' 条新评论.', diffInfo.diffMap, ]); } } function mergeMap(baseMap, diffMap) { for (var key in diffMap) { baseMap[key] = diffMap[key]; } } function getMapping() { var mapping = GM_getValue(mappingKey); if (!mapping) { mapping = {}; } return mapping; } function updateMapping(diffMap) { var mapping = getMapping(); mergeMap(mapping, diffMap); GM_setValue(mappingKey, mapping); GM_setValue(unACKDataKey, null); GM_setValue(unACKTimeKey, null); log([ 'from updateMapping', '写入 mapping:', mapping, ]); } function grabUserInfo() { var uid = GM_getValue(uidKey); if (uid) { g.uid = uid; return; } log('try to get uid from page...'); var userInfo = $('span.user-infoWraptwo'); if (!userInfo) { log('no userinfo span, unable to get uid.'); return; } var userData = $(userInfo).text(); var uidPat = /UID:\s(\d+)/; var m = uidPat.exec(userData); if (m && m.length) { uid = m[1]; log(`got uid: ${uid}.`); g.uid = uid; GM_setValue(uidKey, uid); } } // 自己发的评论不会导致回复数增加 function addSelfCommentCallback() { var form = $('form[name=FORM]'); if (!form) { log('no form, no worries.'); return; } var modifyPat = /post.php\?action-modify/; // 编辑回复页面,不增加回复数 if (modifyPat.test(g.url)) { log('modify page...'); return; } $(form).on('submit', function () { var tid = $('form > input[name=tid]').attr('value'); log(`tid: ${tid}`); if (!tid) { log('发帖页面,不是评论,无 tid'); return; } // 评论 var mapping = GM_getValue(mappingKey); if (!mapping) { mapping = {}; } // 可能是自己刚刚发的帖子,也可能是别人的帖子 // 给刚刚发的帖子评论就无视掉好了 if (!(tid in mapping)) { log(`对 tid=${tid} 的帖子评论.`); } else { mapping[tid] += 1; GM_setValue(mappingKey, mapping); log('回复自己的帖子.') } }); } // 5. UI // 给【我的主题】按钮添加停止闪烁的回调函数,每个页面只要做一次就行了 function addStopBlinkCallback() { var myPostButton = $('#infobox').find('.link5')[0]; // 有些页面没有这个按钮,不需要更新 UI。 if (!myPostButton) { g.has_mypost_btn = false; return; } else { g.has_mypost_btn = true; } $(myPostButton).wrap(''); var span = $(myPostButton).parent()[0]; addSpinEffect(); // enforce event capture, disable event bubbling. span.addEventListener('click', function (e) { var unACKData = GM_getValue(unACKDataKey); if (unACKData) { updateMapping(unACKData.diffMap); } g.stopNofitication = true; }, true); } function addSpinEffect() { $('head').append('' ); } function sendNotification(diffInfo) { if (!g.has_mypost_btn) { return; } log([ 'from sendNotification', 'stopNofitication:' + g.stopNofitication, 'unInitialized: ' + g.unInitialized, ]); var nNewComment = diffInfo.nNewComment; if (!nNewComment) { return; } else if(g.nNewCommentCache && g.nNewCommentCache == nNewComment) { return; } else { g.nNewCommentCache = nNewComment; } var title_blk = `【新回复 x ${nNewComment}】${originalTitle}`; var title_list = [originalTitle, title_blk]; var fw_list = ['normal', 'bold']; g.style = { title_list : title_list, fw_list : fw_list, index : 0 }; var myPostButton = $('#infobox').find('.link5')[0]; $(myPostButton).text(`我的主题( ${nNewComment} 条新回复)`); function updateStyle(index) { document.title = g.style.title_list[index]; } function blink() { setTimeout(function () { g.style.index = 1 - g.style.index; updateStyle(g.style.index); if (g.stopNofitication) { updateStyle(0); $('#btn-my-post').attr('id', ''); } else { blink(); } }, 1000); }; if (g.unInitialized) { g.unInitialized = false; blink(); var span = $(myPostButton).parent()[0]; $(span).attr('id', 'btn-my-post'); } } function log(sl, lv = g.logLevel) { if (lv == logLevels.verbose) { if (sl.constructor === Array) { sl.forEach(function (s) { console.log(s); }); } else { console.log(sl); } } } })();