您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自己的
// ==UserScript== // @name 时光机查询小组之外自己的最近发言 // @namespace https://bgm.tv/group/topic/417621 // @version 0.1 // @description 自己的 // @author ooo // @match https://bgm.tv/user/*/groups // @grant none // @license MIT // ==/UserScript== (async function () { 'use strict' const INDEXED_DB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB const BA_FEH_API_URL = 'https://bgm.nyamori.moe/forum-enhance/query' const FACE_KEY_GIF_MAPPING = { "0": "44", "140": "101", "80": "41", "54": "15", "85": "46", "104": "65", "88": "49", "62": "23", "79": "40", "53": "14", "122": "83", "92": "53", "118": "79", "141": "102", "90": "51", "76": "37", "60": "21", "128": "89", "47": "08", "68": "29", "137": "98", "132": "93" } const SPACE_ACTION_BUTTON_WORDING = { "group": "小组讨论统计", "subject": "条目讨论统计", "ep": "章节讨论统计", "character": "角色讨论统计", "person": "人物讨论统计", "blog": "日志发言统计" } const SPACE_TOPIC_URL = { "group": "group/topic", "subject": "subject/topic", "ep": "ep", "character": "character", "person": "person", "blog": "blog" } const userId = window.location.pathname.split('/')[2] if (userId !== $('#dock .first a').attr('href').split('/').pop()) return; const types = ['group', 'subject', 'ep', 'character', 'person', 'blog'] const columnUserSingle = document.querySelector('#columnUserSingle') const frag = document.createDocumentFragment() async function getUserStatObj(username, type) { const BA_FEH_CACHE_PREFIX = `ba_feh_${type}_` if (await areYouCached(username, BA_FEH_CACHE_PREFIX)) { return await getCacheByUsername(username, BA_FEH_CACHE_PREFIX) } const allUsernameSet = getAllUsernameSet() for (const un in allUsernameSet) { if (await areYouCached(un, BA_FEH_CACHE_PREFIX)) { delete allUsernameSet[un] } } const usernameListToFetch = Object.keys(allUsernameSet) console.debug(`[BA_FEH] Fetching: ${JSON.stringify(usernameListToFetch)}`) let fetched = await fetch(BA_FEH_API_URL, { body: JSON.stringify({ users: usernameListToFetch, type }), method: 'POST' }) .then(d => d.json()) .catch(e => console.error('[BA_FEH] Exception when fetching data: ', e, e)) for (const u in fetched) { await storeInCache(u, fetched[u], BA_FEH_CACHE_PREFIX) } return await getCacheByUsername(username, BA_FEH_CACHE_PREFIX) } async function storeInCache(username, userStatObj, BA_FEH_CACHE_PREFIX) { const ck = `${BA_FEH_CACHE_PREFIX}${username}` if (INDEXED_DB) { await getIndexedDBManager().setItem(ck, userStatObj) } else { sessionStorage[ck] = JSON.stringify(userStatObj) } } async function areYouCached(username, BA_FEH_CACHE_PREFIX) { const ck = `${BA_FEH_CACHE_PREFIX}${username}` if (INDEXED_DB) { const statObj = await getIndexedDBManager().getItem(ck) if (!statObj) return false return !isUserStatCacheExpired(statObj) } else if (sessionStorage[ck]) { const statObj = JSON.parse(sessionStorage[ck]) return !isUserStatCacheExpired(statObj) } return false } function isUserStatCacheExpired(userStatObj) { return new Date().valueOf() > (userStatObj?._meta?.expiredAt ?? new Date().valueOf()) } async function getCacheByUsername(username, BA_FEH_CACHE_PREFIX) { const ck = `${BA_FEH_CACHE_PREFIX}${username}` if (INDEXED_DB) { return await getIndexedDBManager().getItem(ck) } return JSON.parse(sessionStorage[ck]) } function getIndexedDBManager() { const DATA_BASE_NAME = 'BA_FEH' const TABLE_NAME = 'CACHE' const UNIQ_KEY = 'BA_FEH_CACHE_KEY' let dataBase = null function getDataBase() { if (dataBase) { return dataBase } return new Promise(resolve => { const request = indexedDB.open(DATA_BASE_NAME) request.onupgradeneeded = e => { const db = e.target.result if (!db.objectStoreNames.contains(TABLE_NAME)) { db.createObjectStore(TABLE_NAME, { keyPath: UNIQ_KEY }) } } request.onsuccess = e => { const db = e.target.result dataBase = db resolve(db) } }) } return { async setItem(key, value) { const dataBase = await getDataBase() return new Promise(resolve => { const request = dataBase.transaction(TABLE_NAME, 'readwrite') .objectStore(TABLE_NAME) .put({ data: value, [UNIQ_KEY]: key }) request.onsuccess = () => resolve('success') }) }, async getItem(key) { const dataBase = await getDataBase() return new Promise(resolve => { const request = dataBase.transaction(TABLE_NAME) .objectStore(TABLE_NAME) .get(key) request.onsuccess = () => { resolve(request.result?.data) } }) }, async deleteItem(key) { const dataBase = await getDataBase() return new Promise(resolve => { const request = dataBase.transaction(TABLE_NAME, 'readwrite') .objectStore(TABLE_NAME) .delete(key) request.onsuccess = () => { resolve(request.result === undefined) } }) }, async keys() { const keys = {} const dataBase = await getDataBase() return new Promise(resolve => { const request = dataBase.transaction(TABLE_NAME) .objectStore(TABLE_NAME) .openCursor() request.onsuccess = () => { const cursor = request.result if (cursor) { cursor.continue() keys[cursor.value[UNIQ_KEY]] = true } else { resolve(keys) } } }) } } } async function purgeCache() { if (!INDEXED_DB) return let timing = new Date().valueOf() const dbMgr = getIndexedDBManager() const keys = await dbMgr.keys() let ctr = 0 const deleted = [] console.debug(`[BA_FEH] Keys before purging cache: ${JSON.stringify(Object.keys(keys))}`) for (const k in keys) { const statObj = await dbMgr.getItem(k) if (!statObj) continue if (isUserStatCacheExpired(statObj)) { await dbMgr.deleteItem(k) ctr++ deleted.push(k) } } timing = new Date().valueOf() - timing console.debug(`[BA_FEH] The following expired cache keys has been removed in db: ${JSON.stringify(deleted)}`) console.log(`[BA_FEH] Timing for purging cache: ${timing}ms. ${ctr} rows deleted`) } function getAllUsernameSet() { return { [userId]: null } } types.forEach(type => { const details = document.createElement('details') details.innerHTML = `<summary style="font-size:14px;line-height:2;font-weight:bold">${SPACE_ACTION_BUTTON_WORDING[type]}</summary><p class="loading-ind">加载中……</p>` details.dataset.type = type frag.appendChild(details) details.insertAdjacentHTML('afterend', '<div class="clear section_line"></div>') details.addEventListener('toggle', async function () { if (this.open) { const loading = this.querySelector('.loading-ind') if (!loading) return let result try { result = await getUserStatObj(userId, type) } catch (error) { console.error(`Fetch error for ${type}:`, error) this.insertAdjacentHTML('beforeend', `<p>加载失败: ${error.message}</p>`) return } loading.remove() const userStatObj = { ...result, type } const content = drawWrapper(userStatObj) this.insertAdjacentHTML('beforeend', content) } }) }) columnUserSingle.prepend(frag) function drawWrapper(userStatObj) { const spaceType = userStatObj.type const shouldDrawTopicStat = spaceType === 'blog' || SPACE_TOPIC_URL[spaceType].endsWith('topic') const shouldDrawLikesStat = spaceType !== 'blog' && spaceType.length % 3 !== 0 return ` <div class="subject_tag_section" style="margin: 1em;"> <div> <div> <span class="tip">帖子统计:</span> ${drawPostStatData(userStatObj.postStat)} </div> ${shouldDrawTopicStat ? ` <div> <span class="tip">主题统计:</span> ${drawTopicStatData(userStatObj.topicStat)} </div> ` : ''} ${shouldDrawLikesStat ? ` <div> <span class="tip">收到贴贴:</span> ${drawFaceGrid(userStatObj.likeStat)} </div> <div> <span class="tip">送出贴贴:</span> ${drawFaceGrid(userStatObj.likeRevStat)} </div> ` : ''} <div> <span class="tip">空间统计:</span> ${drawSpaceStatSection(userStatObj.spaceStat, spaceType)} </div> <div> ${shouldDrawTopicStat ? ` <span class="tip">最近发表:</span> ${drawRecentTopicSection(userStatObj.recentActivities.topic, spaceType)} <br/> ` : ''} <span class="tip">最近回复:</span> ${drawRecentPostSection(userStatObj.recentActivities.post, spaceType)} ${shouldDrawLikesStat ? ` <br/> <span class="tip">最近送出贴贴:</span> ${drawRecentLikeRevSection(userStatObj.recentActivities.likeRev, spaceType)} ` : ''} </div> </div> </div> ` } function extractSortedListOfFace(faceMap) { const res = [] for (const key in faceMap) { res.push([key, faceMap[key]]) } return res.sort((a, b) => b[1] - a[1]) } function drawFaceGrid(faceMap) { const extracted = extractSortedListOfFace(faceMap) if (extracted.length === 0) return '<span>N/A</span>' let inner = '' for (const p of extracted) { const faceKey = p[0] const faceCount = p[1] const facePicValue = FACE_KEY_GIF_MAPPING[faceKey] inner += ` <a class="item" data-like-value="${faceKey}"> <span class="emoji" style="background-image: url('/img/smiles/tv/${facePicValue}.gif');"></span> <span class="num">${faceCount}</span> </a> ` } return ` <div class="likes_grid" style="float: none;"> ${inner} </div> ` } function drawPostStatData(postStatObj) { return ` <small class="grey"> ${postStatObj.total}(T) ${postStatObj.r7d > 0 ? `/<span>${postStatObj.r7d}(7d)</span>` : ''} ${postStatObj.r30d > 0 ? `/<span>${postStatObj.r30d}(30d)</span>` : ''} ${postStatObj.deleted > 0 ? `/<span style="color: red;">${postStatObj.deleted}(D)</span>` : ''} ${postStatObj.adminDeleted > 0 ? `/<span style="color: yellowgreen;">${postStatObj.adminDeleted}(AD)</span>` : ''} ${postStatObj.violative > 0 ? `/<span style="color: rgb(50, 255, 245);">${postStatObj.violative}(V)</span>` : ''} ${postStatObj.collapsed > 0 ? `/<span style="color: rgb(89, 116, 252);">${postStatObj.collapsed}(F)</span>` : ''} </small> ` } function drawLikeStatData(likeStatForSpaceObj) { return ` <small class="grey"> ${likeStatForSpaceObj.total}(T) </small> ` } function drawSpaceStatData(spaceStatObj, spaceType) { let { name, displayName, topic, post, like, likeRev } = spaceStatObj let isNameTooLong = displayName.length > 10 displayName = displayName.substring(0, Math.min(10, displayName.length)) if (isNameTooLong) displayName += '...' const topicDrawing = drawTopicStatData(topic) const postDrawing = drawPostStatData(post) const likeRevDrawing = drawLikeStatData(likeRev) const likeDrawing = drawLikeStatData(like) let spacePath = '' switch (spaceType) { case 'blog': spacePath = 'user' break case 'ep': spacePath = 'subject' break default: spacePath = spaceType } const shouldDrawTopicStat = spaceType === 'blog' || SPACE_TOPIC_URL[spaceType].endsWith('topic') const shouldDrawLikesStat = spaceType !== 'blog' && spaceType.length % 3 !== 0 return ` <div> <a href="/${spacePath}/${name}" class="l" target="_blank" rel="nofollow external noopener noreferrer">${displayName}</a> <span class="tip">帖子:</span> ${postDrawing} ${shouldDrawTopicStat ? ` <span class="tip">主题:</span> ${topicDrawing} ` : ''} ${shouldDrawLikesStat ? ` <span class="tip">送出贴贴:</span> ${likeRevDrawing} <span class="tip">收到贴贴:</span> ${likeDrawing} ` : ''} </div> ` } function drawSpaceStatSection(spaceStatObjList, spaceType) { if (spaceStatObjList.length === 0) return '<span>N/A</span>' let inner = '' for (const s of spaceStatObjList) { inner += drawSpaceStatData(s, spaceType) } return ` <div class="subject_tag_section"> ${inner} </div> ` } function drawRecentTopic(topicBriefObj, spaceType) { return `<a class="l inner" target="_blank" rel="nofollow external noopener noreferrer" href="/${SPACE_TOPIC_URL[spaceType]}/${topicBriefObj.id}" title="${topicBriefObj.spaceDisplayName || ''}" > ${topicBriefObj.title} <small class="grey">${formatDateline(topicBriefObj.dateline)}</small></a>` } function drawRecentTopicSection(recentTopicObjList, spaceType) { if (recentTopicObjList.length === 0) return `<span>N/A</span>` let inner = '' for (const t of recentTopicObjList) { inner += drawRecentTopic(t, spaceType) } return ` <div class="subject_tag_section"> ${inner} </div> ` } function formatDateline(dateline) { let msWithOffset = 1000 * (dateline - new Date().getTimezoneOffset() * 60) let d = new Date(msWithOffset) let [year, month, day] = d.toISOString().split('T')[0].split('-') return `${year.substring(2)}${month}${day}` } function drawRecentLikeRev(likeRevBrief, spaceType) { let likeRevObjListHtml = '' for (const l of likeRevBrief.likeRevList) { likeRevObjListHtml += ` <a target="_blank" rel="nofollow external noopener noreferrer" href="/${SPACE_TOPIC_URL[spaceType]}/${likeRevBrief.mid}#post_${l.pid}"> <img style="width: 18px;height: 18px;" src="/img/smiles/tv/${FACE_KEY_GIF_MAPPING[l.faceKey]}.gif"></img> </a> ` } return `<p><a class="l inner" target="_blank" rel="nofollow external noopener noreferrer" href="/${SPACE_TOPIC_URL[spaceType]}/${likeRevBrief.mid}" title="${likeRevBrief.spaceDisplayName || ''}" > ${likeRevBrief.title} <small class="grey"> ${formatDateline(likeRevBrief.dateline)} </small> </a><small class="grey">:</small>${likeRevObjListHtml}</p>` } function drawRecentLikeRevSection(recentLikeRevObjList, spaceType) { if (recentLikeRevObjList.length === 0) return `<span>N/A</span>` let inner = '' for (const t of recentLikeRevObjList) { inner += drawRecentLikeRev(t, spaceType) } return ` <div class="subject_tag_section"> ${inner} </div> ` } function drawRecentPost(postBriefObj, spaceType) { return `<a class="l inner" target="_blank" rel="nofollow external noopener noreferrer" href="/${SPACE_TOPIC_URL[spaceType]}/${postBriefObj.mid}#post_${postBriefObj.pid}" title="${postBriefObj.spaceDisplayName || ''}" > ${postBriefObj.title} <small class="grey">${formatDateline(postBriefObj.dateline)}</small></a>` } function drawRecentPostSection(recentPostObjList, spaceType) { if (recentPostObjList.length === 0) return `<span>N/A</span>` let inner = '' for (const p of recentPostObjList) { inner += drawRecentPost(p, spaceType) } return ` <div class="subject_tag_section"> ${inner} </div> ` } function drawTopicStatData(topicStatObj) { return ` <small class="grey"> ${topicStatObj.total}(T) ${topicStatObj.r7d > 0 ? `/<span>${topicStatObj.r7d}(7d)</span>` : ''} ${topicStatObj.r30d > 0 ? `/<span>${topicStatObj.r30d}(30d)</span>` : ''} ${topicStatObj.deleted > 0 ? `/<span style="color: red;">${topicStatObj.deleted}(D)</span>` : ''} ${topicStatObj.silent > 0 ? `/<span style="color: rgb(255, 145, 0);">${topicStatObj.silent}(S)</span>` : ''} ${topicStatObj.closed > 0 ? `/<span style="color: rgb(164, 75, 253);">${topicStatObj.closed}(C)</span>` : ''} ${topicStatObj.reopen > 0 ? `/<span style="color: rgb(53, 188, 134);">${topicStatObj.reopen}(R)</span>` : ''} </small> ` } })()