Twitch User Chat Log

Twitchで他のユーザーのチャット履歴を見ることができるスクリプトです.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch User Chat Log
// @namespace    TwitchUserChatLogScript
// @version      1.1
// @description  Twitchで他のユーザーのチャット履歴を見ることができるスクリプトです.
// @author       Emble
// @match        https://www.twitch.tv/*
// @require      https://code.jquery.com/jquery-3.3.1.min.js
// @noframes
// @grant        none
// ==/UserScript==

(function() {
    var intervalID = []
    let retryCounter = [0, 0]
    const Page = {
        isStreaming: (location.pathname.indexOf('video') !== -1)? false : true,
        getChatArea: ()=>{return (location.pathname.indexOf('video') !== -1)?document.getElementsByClassName('tw-align-items-end tw-flex tw-flex-wrap tw-full-width')[0]:document.getElementsByClassName('chat-scrollable-area__message-container tw-flex-grow-1 tw-pd-b-1')[0]},
        putOpenPanelButton: ()=>{
            if(!Page.checkExistElementClass('tw-flex tw-flex-grow-1 tw-flex-wrap tw-mg-b-05 tw-mg-l-1')){
                return
            }
            const prt = document.getElementsByClassName('tw-flex tw-flex-grow-1 tw-flex-wrap tw-mg-b-05 tw-mg-l-1')[0]
            const btn = document.createElement('div')
            btn.className = 'tw-mg-r-05 tw-mg-t-05'
            btn.innerHTML = HTML.logButton

            prt.appendChild(btn)
            return
        },
        putClosePanelButton: ()=>{
            if(!Page.checkExistElementId('TUCL_top_bar') || Page.checkExistElementId('TUCL_button_close_panel')){
                return
            }
            const prt = document.getElementById('TUCL_top_bar')
            const btn = document.createElement('button')
            btn.id = 'TUCL_button_close_panel'
            btn.innerHTML = '<span>X</span>'
            btn.style.width = '20px'
            btn.style.height = '20px'
            btn.style.zIndex = '2'
            btn.onclick = Page.closeLogPanel

            prt.appendChild(btn)
        },
        putEvent: ()=>{
            document.getElementById('TUCL_button_open_panel').onclick = Page.createLogPanel
            return
        },
        createLogPanel: ()=>{
            if(Page.checkExistElementId('TUCL_LogPanel')){
                Page.clearChatLog()
                Page.drawChatLog()
                return
            }
            const pr = document.getElementsByClassName('chat-room__content tw-c-text-base tw-flex tw-flex-column tw-flex-grow-1 tw-flex-nowrap tw-full-height tw-relative')[0]
            const pl = document.createElement('div')
            pl.id = 'TUCL_LogPanel'
            pl.style.background = 'var(--color-background-body)'
            pl.style.overflowY = 'scroll'
            pl.style.whiteSpace = 'normal'
            pl.style.position = 'absolute'
            pl.style.width = '350px'
            pl.style.height = '400px'
            pl.style.left = '-300px'
            pl.style.opacity = '90%'
            pl.style.zIndex = '1'
            pl.innerHTML = '<div id="TUCL_top_bar"></div><div id="TUCL_chat_log_list"></div>'

            pr.appendChild(pl)

            Page.drawChatLog()
            Page.putClosePanelButton()
        },
        closeLogPanel: ()=>{
            const el = document.getElementById('TUCL_LogPanel')
            el.parentNode.removeChild(el)
        },
        clearChatLog: ()=>{
            if(!Page.checkExistElementId('TUCL_chat_log_list')){
                return
            }
            document.getElementById('TUCL_chat_log_list').innerHTML = ''
        },
        getTargetUserName: () => {
            const el = document.getElementsByClassName('tw-link tw-link--hover-color-inherit tw-link--hover-underline-none tw-link--inherit')[0]
            const href = el.getAttribute('href')
            console.log(href.substr(1))
            return href.substr(1)
        },
        drawChatLog: () => {
            if(!Page.checkExistElementId('TUCL_chat_log_list')){
                return
            }
            const name = Page.getTargetUserName()
            //名前の検索
            const array = JSON.parse(sessionStorage.getItem('TUCL_Log'))
            let chatLog = []
            for(let key of Object.keys(array)){
                if(key == name){
                    chatLog = array[key]
                    break
                }
            }
            let html = ''
            chatLog.map((chat)=>{
                html += '<span style="font-size: 16px;color: var(--color-text-base);display: block;border-bottom: solid 1px var(--color-text-base);">'+chat+'</span>'
            })

            const panel = document.getElementById('TUCL_chat_log_list')
            panel.innerHTML = html
            console.log(chatLog)
        },
        waitUserCardLoading: ()=>{
            clearInterval(intervalID[1])
            intervalID[1] = setInterval(()=>{
                if(Page.checkExistElementClass('tw-flex tw-flex-grow-1 tw-flex-wrap tw-mg-b-05 tw-mg-l-1')){
                    if(!Page.checkExistElementId('TUCL_Label')){
                        Page.putOpenPanelButton()
                        Page.putEvent()
                    }
                    Page.clearChatLog()
                    Page.drawChatLog()

                    clearInterval(intervalID[1])
                }
                if(retryCounter[1] > 10){
                    clearInterval(intervalID[1])
                }
            },500)
        },
        observeUserCard: new MutationObserver((ms)=>{
            ms.forEach((e) => {
                const addedElements = e.addedNodes
                for(let i = 0; i < addedElements.length; i++){
                    if(addedElements[i].getAttribute('data-a-target') === 'viewer-card-positioner'
                       || addedElements[i].className==='tw-border-radius-medium tw-c-background-base tw-elevation-2 tw-flex tw-flex-column viewer-card'){
                        Page.waitUserCardLoading()
                    }
                }
            })
        }),
        checkExistElementId: (el) => {
            if(document.getElementById(el) !== null){
                return true
            }
            return false
        },
        checkExistElementClass: (el) => {
            if(document.getElementsByClassName(el).length > 0){
                return true
            }
            return false
        }
    }

    const HTML = {
        logButton: `<div class="tw-inline-flex viewer-card-drag-cancel" id="TUCL_Label">
  <button class="ScCoreButton-sc-1qn4ixc-0 ScCoreButtonPrimary-sc-1qn4ixc-1 jeBpig tw-core-button" data-a-target="usercard-whisper-button" data-test-selector="whisper-button" id="TUCL_button_open_panel">
    <div class="ScCoreButtonLabel-lh1yxp-0 bUTtZU tw-core-button-label">
      <div class="tw-align-items-center tw-flex tw-mg-r-05">
        <div class="ScCoreButtonIcon-khv8ri-0 fVWBSS tw-core-button-icon">
          <div class="ScIconLayout-sc-1bgeryd-0 kbOjdP tw-icon" data-a-selector="tw-core-button-icon">
            <div class="ScAspectRatio-sc-1sw3lwy-1 dNNaBC tw-aspect">
              <div class="ScAspectSpacer-sc-1sw3lwy-0 gkBhyN">
              </div>
              <svg width="100%" height="100%" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" class="ScIconSVG-sc-1bgeryd-1 cMQeyU"><g><path fill-rule="evenodd" d="M7.828 13L10 15.172 12.172 13H15V5H5v8h2.828zM10 18l-3-3H5a2 2 0 01-2-2V5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2l-3 3z" clip-rule="evenodd"></path></g></svg>
            </div>
          </div>
        </div>
      </div>
      <div data-a-target="tw-core-button-label-text" class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-content-start">
        履歴
      </div>
    </div>
  </button>
</div>`
    }

    const Chat = {
        Observer: new MutationObserver((ms) => {
            ms.forEach((e) => {
                const addedChats = e.addedNodes
                for(let i = 0; i < addedChats.length; i++){
                    if(addedChats[i].className === 'tw-accent-region'){
                        continue
                    }

                    const chatData = Chat.convertChat(addedChats[i])
                    addStorage(chatData.userName, chatData.chat)
                }
            })
        }),
        convertChat: (chat) =>{
            let Data = {
                chat: null,
                userName: null
            }
            const getUserName = () => {
                if(chat.getElementsByClassName('chat-author__intl-login').length > 0){
                    const name = chat.getElementsByClassName('chat-author__intl-login')[0].innerHTML
                    return name.match(/(?<=\().*?(?=\))/)[0]
                }
                if(chat.getElementsByClassName('chat-author__display-name').length > 0){
                    return chat.getElementsByClassName('chat-author__display-name')[0].innerHTML
                }
                return null
            }
            Data.chat = (chat.getElementsByClassName('text-fragment')[0])?chat.getElementsByClassName('text-fragment')[0].innerHTML:null
            Data.userName = getUserName()

            return Data
        }
    }

    const findChatArea = (c) => {
        intervalID[0] = setInterval(()=>{
            let chatAreaElement = Page.getChatArea()
            if(chatAreaElement !== undefined ){
                log('Found element')

                initialize()
                clearInterval(intervalID[0])
            }
            if(retryCounter[0] > c){
                log('Element not found.')

                clearInterval(intervalID[0])
            }
            retryCounter++
        },1000)
    }
    const runObserver = () => {
        if(Page.getChatArea() === undefined || null)return
        if(document.getElementsByClassName('tw-full-height tw-full-width tw-relative tw-z-above viewer-card-layer').length <= 0)return

        Page.observeUserCard.disconnect()
        Page.observeUserCard.observe(document.getElementsByClassName('tw-full-height tw-full-width tw-relative tw-z-above viewer-card-layer')[0],
                                     {childList: true,
                                     characterData: true,
                                     subtree: true})
        Chat.Observer.disconnect()
        Chat.Observer.observe(Page.getChatArea(), {childList: true})
    }
    const initialize = () => {
        if(!sessionStorage.getItem('TUCL_Log')){
            let array = {}
            sessionStorage.setItem('TUCL_Log', JSON.stringify(array))
        }

        runObserver()
    }
    const addStorage = (name, chat) =>{
        /*
        * let array = {
        *   "user1": ["chat1", "chat2"],
        *   "user2": ["chatA", "chatB", "chatC"]
        *    .
        *    .
        *    .
        * }
        */

        //名前の検索
        let array = JSON.parse(sessionStorage.getItem('TUCL_Log'))
        for(let key of Object.keys(array)){
            if(key == name){
                let val = array[key]
                val.push(chat)
                array[key] = val
                sessionStorage.setItem('TUCL_Log', JSON.stringify(array))
                return
            }
        }
        //見つからなければ新規追加
        array[name] = []
        let val = array[name]
        val.push(chat)
        array[name] = val
        sessionStorage.setItem('TUCL_Log', JSON.stringify(array))
    }
    const getStorage = (n) => {
        return JSON.parse(sessionStorage.getItem('TUCL_Log'))
    }
    const openLogWindow = () => {

    }
    let storedHref = location.href;
    const URLObserver = new MutationObserver(function(ms){
        ms.forEach(function(m){
            if(storedHref !== location.href){
                storedHref = location.href
                log('URL Changed', storedHref, location.href)

                Chat.Observer.disconnect()
                clearInterval(intervalID[0])
                findChatArea(60)
            }
        })
    })
    const log = (m) => console.log('[TUCL] '+m)

    window.onload = () => {
        if(!sessionStorage){
            alert('セッションストレージ非対応ブラウザです。')
            return
        }
        URLObserver.observe(document, {childList: true, subtree: true})
        findChatArea(60)
    }
})()