Twitch Chat Filter

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

目前為 2023-12-06 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Twitch Chat Filter
// @namespace    TwitchChatFilterScript
// @version      0.6
// @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())?"chat-line__message":"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"},
    }

    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);
    }
})();