Greasy Fork is available in English.

时光机查询小组之外自己的最近发言

自己的

// ==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>
      `
    }

})()