Greasy Fork is available in English.

Twitch Chat Filter

Twitchのチャット欄にNG機能を追加します。Chat Filter for Twitch chat

// ==UserScript==
// @name         Twitch Chat Filter
// @namespace    TwitchChatFilterScript
// @version      0.8
// @description  Twitchのチャット欄にNG機能を追加します。Chat Filter for Twitch chat
// @author       bd
// @match        https://www.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?domain=twitch.tv
// @license      MIT
// @noframes
// @grant           GM_setValue
// @grant           GM_getValue
// ==/UserScript==

(function() {
    const Config = {
        BannedWord: GM_getValue("TCO_BannedWord"),
        BannedUser: GM_getValue("TCO_BannedUser"),
        AutoBan: false,

        Load: () => {
            if(Config.BannedWord == null){
                Config.BannedWord = "";
            }
            if(Config.BannedUser == null){
                Config.BannedUser = "";
            }
        },
        Save: () => {
            GM_setValue("TCO_BannedWord", Config.BannedWord);
            GM_setValue("TCO_BannedUser", Config.BannedUser);
        },
        AddBannedWord: (word) => {
            Config.BannedWord += word + "\n";
        },
        AddBannedUser: (id) => {
            Config.BannedUser += id + "\n";
        },
    }

    const ChatFieldObserver = new MutationObserver(function(mutations){
        mutations.forEach(function(e){
            let chat = e.addedNodes;
            //console.log(chat);
            for(let i = 0; i < chat.length; i++){
                if(chat[i].className != ClassName.AddedChat()){
                    continue;
                }
                try {
                    const userInfo = GetUserInfo(chat[i]);
                    const textContainer = GetChildElementsByAttribute(
                        Element.GetMessageElement(chat[i]),
                        AttributeName.TextContainer(),
                        AttributeName.TextContainerValue()
                    );//ここまで

                    if(IsBannedWord(textContainer[0].innerText) || IsBannedUser(userInfo.id) || IsBannedWorldPerfect(textContainer[0].innerText)){
                        HideElement(chat[i]);
                        ShowBannedChat(textContainer[0].innerText, userInfo.id);
                        AddBannedCount();
                    }



                    if(IsBannedWord(textContainer[0].innerText)){
                        if(Config.AutoBan && !IsBannedUser(userInfo.id)){
                            Config.AddBannedUser(userInfo.id);
                            Config.Save();

                            LoadPanelValue();
                            console.log('added');
                        }
                    }

                    PutBanButton(textContainer[0]);
                    SetBanButtonEvent(chat[i], userInfo.id);

                    //console.log(userInfo.name);
                    //console.log(userInfo.id);
                    //console.log(textContainer);
                }
                catch ( e ) {
                    console.error(e.message);
                }
                finally{
                    continue;
                }
            }
        })
    });

    //頻繁に変わりそうなクラス名など
    const ClassName = {
        //配信時:アーカイブ時
        ChatField: ()=>{return (isStreaming())?"chat-scrollable-area__message-container":"video-chat__message-list-wrapper"},
        DisplayName: "chat-author__display-name",
        AddedChat: ()=>{return (isStreaming())?"Layout-sc-1xcs6mc-0":"InjectLayout-sc-1i43xsx-0 bQEtql"},
        ChatMessageContainer: () => {return (isStreaming())?"chat-line__no-background":"video-chat__message"},
        BottomBar: () => {return (isStreaming())?"Layout-sc-1xcs6mc-0 bKPhAm":"Layout-sc-1xcs6mc-0 bZpfnT"},

    }

    const AttributeName = {
        TextContainer: () => {return (isStreaming())?"data-a-target":"data-a-target"},
        //TextContainerValue: () => {return (isStreaming())?"chat-message-text":"chat-message-text"},
        TextContainerValue: () => {return (isStreaming())?"chat-line-message-body":"chat-message-text"},
    }

    const Element = {
        GetChatField: () => {
            return (isStreaming())?
                document.getElementsByClassName(ClassName.ChatField())[0]:
            document.getElementsByClassName(ClassName.ChatField())[0].firstChild.firstChild
        },
        GetMessageElement: (chat) => {
            return (isStreaming())?
                chat.getElementsByClassName(ClassName.ChatMessageContainer())[0].lastChild:
            chat.getElementsByClassName(ClassName.ChatMessageContainer())[0].lastChild
        }
    }

    //現在のページの配信状態を判別
    let isStreaming = () =>{
        let pathname = location.pathname;
        let path = pathname.split('/');
        return path.length === 2;

        /*
        // 配信時
        if(path.length == 2){
            return true;
        }
        // 配信なしorアーカイブ
        else{
            return false;
        }
        return false;*/
    };

    let settingPanelActive = false;
    let bannedCount = 0;
    let waitInterval;

    window.onload = function() {
        console.log(location.pathname);
        console.log(isStreaming());
        WaitPageLoaded();
    }

    function WaitPageLoaded()
    {
        let count = 1;
        clearInterval(waitInterval);

        waitInterval = setInterval(function(){
            count++;

            //console.log(ClassName.ChatField());

            //発見時
            if(Element.GetChatField() !== undefined &&
              document.getElementsByClassName(ClassName.BottomBar())[0] !== undefined){
                log('Element detected.');
                Initialize();

                count = 0;
                clearInterval(waitInterval);
            }

            //発見不可
            if(10 < count){
                log('Element cannot be found.');

                count = 0;
                clearInterval(waitInterval);
            }

        },1000);
    }

    function Initialize(){
        Config.Load();

        ChatFieldObserver.disconnect();
        ChatFieldObserver.observe(
            Element.GetChatField(),
            {childList: true}
        );
        //console.log(Element.GetChatField());

        PutSettingPanel();
        SetPanelEvent();
        LoadPanelValue();
        SetAutoBanEvent();
    }

     //NGワードの判定する
    function IsBannedWord(text){
        if(Config.BannedWord == ""){
            return false;
        }
        let BannedWord = Config.BannedWord.split(/\r\n|\n/);

        for(let i = 0; i < BannedWord.length; i++){
            if(BannedWord[i] == ""){
                continue;
            }
            let result = text.match(BannedWord[i])
            if(result != null){
                return true;
                break;
            }
        }
        return false;
    }

    function IsBannedWorldPerfect(text){
        if(text == "あ" || text == "a"){
            return true;
        }
        else{
            return false;
        }
    }


    //NGユーザの判定する
    function IsBannedUser(id){
        if(Config.BannedUser == ""){
            return false;
        }
        let BannedUser = Config.BannedUser.split(/\r\n|\n/);

        for(let i = 0; i < BannedUser.length; i++){
            if(BannedUser[i] == ""){
                continue;
            }
            let result = id.match(BannedUser[i])
            if(result != null){
                return true;
                break;
            }
        }
        return false;
    }

    //指定のエレメントを非表示
    function HideElement(element){
        element.style.display = "none";
    }

    function ShowBannedChat(text, id){
        if(15 < text.length)text = text.substr(0, 15) + "..";

        let html = document.getElementById("tco-banned-chat").innerHTML;
        html = id + ": " + text + "\n" + html;

        document.getElementById("tco-banned-chat").innerHTML = html.substr(0, 150);
    }

    function PutSettingPanel(){
        const bottomBar = document.getElementsByClassName(ClassName.BottomBar())[0];
        const HTML =`<div class="tco-panel" id="tco-panel">
    <button class="ScCoreButton-sc-1qn4ixc-0 jGqsfG ScButtonIcon-sc-o7ndmn-0 fNzXyu" data-a-target="setting-panel-button" id="tco-panel-button">
        <span>設定</span>
    </button>
    <div class="tco-panel-background" id="tco-panel-background" style="
    position: absolute;
    top: -300px;
    width: 340px;
    height: auto;
    left: 500px;
    background-color: black;
    opacity: 0.8;
    display: none;
    flex-direction: row;
    justify-content: center;
">
        <div style="
    display: flex;
    flex-direction: column;
    width: 100%;
">
            <span>NGワード<font color="red">*</font></span>
            <div>
                <textarea name="tco-banned-words" id="tco-banned-words" rows="8"></textarea>
            </div>
            <span>NGユーザー<font color="red">*</font></span><span id="tco-users-count">人</span>
            <div>
                <textarea name="tco-banned-users" id="tco-banned-users" rows="8"></textarea>
            </div>
            <div>
                <input type="checkbox" class="tco-input-checkbox" id="tco-input-checkbox-put-button" checked="true"><label for="tco-input-checkbox-put-button">NGボタンを表示する</label>
            </div>
            <div>
                <input type="checkbox" class="tco-input-checkbox" id="tco-input-checkbox-auto-ban"><label for="tco-input-checkbox-auto-ban">NGワードの発言者を自動でNGユーザーに追加</label>
            </div>
        </div>
        <div style="
    display: flex;
    flex-direction: column;
    width: 100%;
">
            <span id="tco-banned-count">0個のゴミを非表示にしました</span>
            <span>↓以下ゴミ共のコメント↓</span>
            <span id="tco-banned-chat" style="white-space: pre-line;color: darkgrey;font-size: 11px;"></span>
            <div style="
    height: 100%;
    display: flex;
    align-items: flex-end;
    justify-content: flex-end;
    margin: 10px;">
            <input type="button" class="tco-save-button" id="tco-save-button" value="保存">
            </div>
        </div>
    </div>
</div>
`
        bottomBar.insertAdjacentHTML("afterbegin", HTML)
    }

    function LoadPanelValue(){
        document.getElementById("tco-banned-words").value = Config.BannedWord;
        document.getElementById("tco-banned-users").value = Config.BannedUser;

        let bannedUser = Config.BannedUser.split(/\r\n|\n/);
        document.getElementById("tco-users-count").innerHTML = Config.BannedUser.split(/\r\n|\n/).length.toString() + "人";
    }

    function GetPanelInfo(){
        let _bannedWord = document.getElementById("tco-banned-words").value;
        let _bannedUser = document.getElementById("tco-banned-users").value;

        let result = {
            bannedWord: _bannedWord,
            bannedUser: _bannedUser,
        }

        return result;
    }

    //設定パネルのイベントなどを設定
    function SetPanelEvent(){
        document.getElementById("tco-panel-button").onclick = () => {
            if(settingPanelActive){
                document.getElementById("tco-panel-background").style.display = "none";
                settingPanelActive = false;
            }else{
                document.getElementById("tco-panel-background").style.display = "flex";
                settingPanelActive = true;
            }
        }
        document.getElementById("tco-save-button").onclick = () => {
            const panelInfo = GetPanelInfo();
            Config.BannedWord = panelInfo.bannedWord;
            Config.BannedUser = panelInfo.bannedUser;
            Config.Save();

            LoadPanelValue();
        }
    }


    //チャットに表示するNGボタンを設置
    function PutBanButton(container){
        const html =
            `<span style="left: 90%;">
               <button aria-label="NGに入れる" class="tco-ban-button" id="tco-ban-button" style="padding: 0px;width: 14px; height: 14px;" >
                 <div style="width: 100%; height: 14px;">
                   <div class="tw-align-items-center tw-full-width tw-icon tw-icon--fill tw-inline-flex">
                     <svg class="tw-icon__svg" width="100%" height="100%" version="1.1" viewBox="0 0 512 512" x="0px" y="0px" style="fill: var(--color-fill-button-icon-hover);"><path d="M437.023,74.977c-99.984-99.969-262.063-99.969-362.047,0c-99.969,99.984-99.969,262.063,0,362.047c99.969,99.969,262.078,99.969,362.047,0S536.992,174.945,437.023,74.977z M137.211,137.211c54.391-54.391,137.016-63.453,201.016-27.531L109.68,338.227C73.758,274.227,82.82,191.602,137.211,137.211z M374.805,374.789c-54.391,54.391-137.031,63.469-201.031,27.547l228.563-228.563C438.258,237.773,429.18,320.414,374.805,374.789z" fill-rule="evenodd"></path></svg>
                   </div>
                 </div>
               </div>
             </button>
             </span>`;
        container.insertAdjacentHTML("afterend",html);
    }

    function SetBanButtonEvent(chat, id){
        chat.getElementsByClassName("tco-ban-button")[0].onclick = () =>{
            HideElement(chat);
            Config.AddBannedUser(id);
            Config.Save();

            LoadPanelValue();
        };
    }

    function ToggleAutoBan(){
        const checkbox = document.getElementById('tco-input-checkbox-auto-ban');
        Config.AutoBan = checkbox.checked;
    }

    function SetAutoBanEvent(){
        const checkbox = document.getElementById('tco-input-checkbox-auto-ban');
        checkbox.addEventListener('click', ToggleAutoBan);
    }

    function AddBannedCount(){
        bannedCount++;
        document.getElementById("tco-banned-count").innerHTML = bannedCount + "個のゴミを非表示にしました";
    }

    //チャット要素のメッセージ内容を取得し、HTML化して返す。
    function GetChatMessage(chat){
        const messageContainer = document.getElementsByClassName(ClassName.ChatMessageContainer())[0];
        //console.log(messageContainer);
    }

    //チャット要素のユーザー情報を取得し返す
    function GetUserInfo(chat){
        //console.log(chat.getElementsByClassName(ClassName.DisplayName)[0]);
        const _name = chat.getElementsByClassName(ClassName.DisplayName)[0].textContent;
        const _id = chat.getElementsByClassName(ClassName.DisplayName)[0].getAttribute("data-a-user");
        //console.log(_name);
        //console.log(_id);

        const result = {
            name: _name,
            id: _id
        };

        return result;
    }

    //チャット要素の子要素を属性値で絞り、結果をArrayで返します。
    function GetChildElementsByAttribute(element, attribute, value){
        let result = [];
        element.childNodes.forEach((e) => {
            if(e.getAttribute(attribute) == value){
                result.push(e);
            }
        });

        return result;
    }

    function log(text){
        console.log("【TCO】"+text);
    }
})();