Twitch User Chat Log

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

// ==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)
    }
})()