Twitch User Chat Log

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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)
    }
})()