Viewer list bot remover

Places bots in their own category in the viewer list.

اعتبارا من 01-04-2023. شاهد أحدث إصدار.

// ==UserScript==
// @name         Viewer list bot remover
// @namespace    https://greasyfork.org/scripts?set=586193
// @version      1.2.0
// @description  Places bots in their own category in the viewer list.
// @author       Sonyo
// @match        http*://www.twitch.tv/*
// @grant        none
// @license      MIT
// @icon         https://cdn-icons-png.flaticon.com/512/9092/9092067.png
// ==/UserScript==

/*
 * OPTIONS
 * Modify the following variable for the behavior concerning moderator bots:
*/
const modBotsBehavior = 0;
/*
 * 0: Keep them in the Moderators panel
 * 1: Place them with the other bots
 * 2: Place them in their own panel
*/

let viewersPanelName = "";
let moderatorsPanelName = "";
const botsPanelTitle = "Bots";
const botsPanelDescription = "Description for bots... Well they're bots :)";
const botImageSource = "https://cdn-icons-png.flaticon.com/512/9092/9092067.png";
const modBotsPanelTitle = "Moderator bots";
const modBotsPanelDescription = "Bots used for the moderation of this channel.";
const modBotImageSource = "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/2";

// Must keep in alphabetical order !!
const botList = [
    "01ella",
    "0ax2",
    "0niva",
    "24_7_chatting_on_discord",
    "a_ok",
    "abjuui",
    "actually__hannah",
    "adwin666",
    "aliceydra",
    "aliengathering",
    "anotherttvviewer",
    "aten",
    "audycia",
    "avasemaphore",
    "blgdamjudge",
    "buttsbot",
    "captainskrew",
    "commanderroot",
    "creatisbot",
    "digitalinstinct",
    "discordstreamercommunity",
    "drapsnatt",
    "ezobay",
    "freakybabetv",
    "iisabei",
    "jinal",
    "kattah",
    "lucentcrown1234567891011",
    "lurxx",
    "lylituf",
    "maddynsun",
    "moobot",
    "network_streamer_discord",
    "nightbot",
    "ninatela",
    "paradise_for_streamers",
    "pdp_bot",
    "qnfgogtktls",
    "rogueg1rl",
    "roxesy",
    "saralna",
    "soundalerts",
    "srekrapstob",
    "streamelements",
    "thisisunreallol",
    "waptart",
    "wizebot",
    "yamickle",
];

var viewerListShown = false;
var botsPanel = null;

function delay(milliseconds) {
    return new Promise(resolve => {
        setTimeout(resolve, milliseconds);
    });
}

async function detectLanguage() {
    var div = document.querySelector('[class="ScInputBase-sc-vu7u7d-0 ScInput-sc-19xfhag-0 gXVFsI jhrDHh InjectLayout-sc-1i43xsx-0 kvZgmT tw-input tw-input--large"]');
    let count = 0;
    while (div === null) {
        await delay(1000);
        div = document.querySelector('[class="ScInputBase-sc-vu7u7d-0 ScInput-sc-19xfhag-0 gXVFsI jhrDHh InjectLayout-sc-1i43xsx-0 kvZgmT tw-input tw-input--large"]');
        count++;
        if (count > 15) {
            console.log("[Viewer list bot remover]: Search bar not found, script not working");
            return;
        }
    }

    let searchText = div.placeholder;
    switch (searchText) {
        case "Search":
            viewersPanelName = "Viewers";
            moderatorsPanelName = "Moderators";
            break;
        case "Søg":
            viewersPanelName = "Seere";
            moderatorsPanelName = "Moderatorer";
            break;
        case "Suchen":
            viewersPanelName = "Zuschauer";
            moderatorsPanelName = "Moderatoren";
            break;
        case "Buscar":
            viewersPanelName = "Espectadores";
            moderatorsPanelName = "Moderadores";
            break;
        case "Rechercher":
            viewersPanelName = "Spectateurs";
            moderatorsPanelName = "Modérateurs";
            break;
        case "Cerca":
            viewersPanelName = "Spettatori";
            moderatorsPanelName = "Moderatori";
            break;
        case "Keresés":
            viewersPanelName = "Nézők";
            moderatorsPanelName = "Moderátorok";
            break;
        case "Zoeken":
            viewersPanelName = "Kijkers";
            moderatorsPanelName = "Moderators";
            break;
        case "Søk":
            viewersPanelName = "Seere";
            moderatorsPanelName = "Moderatorer";
            break;
        case "Wyszukaj":
            viewersPanelName = "widzowie";
            moderatorsPanelName = "Moderatorzy";
            break;
        case "Pesquisa":
            viewersPanelName = "Espetadores";
            moderatorsPanelName = "Moderadores";
            break;
        case "Căutare":
            viewersPanelName = "Vizualizatori";
            moderatorsPanelName = "Moderatori";
            break;
        case "Hľadať":
            viewersPanelName = "Diváci";
            moderatorsPanelName = "Moderátori";
            break;
        case "Etsi":
            viewersPanelName = "Katsojat";
            moderatorsPanelName = "Moderaattorit";
            break;
        case "Sök":
            viewersPanelName = "Tittare";
            moderatorsPanelName = "Moderatorer";
            break;
        case "Tìm kiếm":
            viewersPanelName = "Người xem";
            moderatorsPanelName = "Người điều hành";
            break;
        case "Ara":
            viewersPanelName = "İzleyici";
            moderatorsPanelName = "Moderatörler";
            break;
        case "Hledat":
            viewersPanelName = "Diváci";
            moderatorsPanelName = "Moderátoři";
            break;
        case "Αναζήτηση":
            viewersPanelName = "Θεατές";
            moderatorsPanelName = "Επόπτες";
            break;
        case "Търсене":
            viewersPanelName = "Зрители";
            moderatorsPanelName = "Модератори";
            break;
        case "Поиск":
            viewersPanelName = "Зрители";
            moderatorsPanelName = "Модераторы";
            break;
        case "ค้นหา":
            viewersPanelName = "ผู้ชม";
            moderatorsPanelName = "ผู้ดำเนินรายการ";
            break;
        case "搜索":
            viewersPanelName = "观众";
            moderatorsPanelName = "管理员";
            break;
        case "搜尋":
            viewersPanelName = "觀眾";
            moderatorsPanelName = "Mod";
            break;
        case "検索":
            viewersPanelName = "視聴者数";
            moderatorsPanelName = "モデレーター";
            break;
        case "검색":
            viewersPanelName = "시청자 수";
            moderatorsPanelName = "매니저";
            break;
        default:
            alert("Unknown language !");
            break;
    }
    //alert(`viewers:${viewersPanelName}, mods:${moderatorsPanelName}`);
}

void async function () {
    'use strict';

    await detectLanguage();

    let prevUrl = undefined;
    setInterval(async () => {
        const currUrl = window.location.href;
        if (currUrl != prevUrl) {
            prevUrl = currUrl;
            await setup();
        }
    }, 60);
}();

async function setup() {
    var communityButton = document.querySelector('[data-test-selector="chat-viewer-list"]');
    let count = 0;
    while (communityButton === null) {
        await delay(1000);
        communityButton = document.querySelector('[data-test-selector="chat-viewer-list"]');
        count++;
        if (count > 15) {
            console.log("[Viewer list bot remover]: Community button not found, script not working");
            return;
        }
    }
    viewerListShown = false;
    communityButton.addEventListener("click", communityButtonClick);
}

async function getContainer() {
    // Get the viewers container
    var scrollable = null;
    let count = 1;
    while (scrollable === null) {
        scrollable = document.querySelector('[class="scrollable-area scrollable-area--suppress-scroll-x"]');

        count++;
        if (count > 300) // 50ms * 300 = 15s
        {
            console.log("[Viewer list bot remover]: Loading took too long");
            return;
        }
        await delay(50);
    }

    return scrollable.lastChild.firstChild.firstChild;
}

function binarySearch(name) {
    let start = 0;
    let end = botList.length - 1;

    while (start <= end) {
        let mid = Math.floor((start + end) / 2);
        if (botList[mid] === name) return true;
        if (botList[mid] < name) start = mid + 1;
        else end = mid - 1;
    }

    return false;
}

function removeBots(panel, container) {
    let viewers = panel.firstChild.lastChild;

    let bots = [];
    for (let i = 0; i < viewers.children.length; i++) {
        let viewer = viewers.children[i];

        let name = viewer.firstChild.firstChild.firstChild.firstChild.textContent;
        let remove = binarySearch(name.toLowerCase());
        if (remove) {
            viewer.remove();
            bots.push(viewer);
            i--;
        }
    }


    // If there is no more viewers, remove the panel
    if (viewers.children.length === 0) {
        panel.remove();
    }

    return bots;
}

function createBotsPanel(panel, container, options) {
    let newPanel = panel.cloneNode(true); // Doesn't copy event listeners FeelsSadMan
    let botImg = document.createElement("img");
    botImg.setAttribute("class", "InjectLayout-sc-1i43xsx-0 lfGYGL tw-image");
    botImg.setAttribute("alt", "Bot badge");
    botImg.setAttribute("src", options.ImageSrc);

    newPanel.firstChild.children[0].firstChild.children[0].remove();
    newPanel.firstChild.children[0].firstChild.insertBefore(botImg, newPanel.firstChild.children[0].firstChild.firstChild);
    newPanel.firstChild.children[0].firstChild.children[1].firstChild.innerHTML = options.Title;
    newPanel.firstChild.children[1].innerHTML = options.Description;
    let viewers = newPanel.firstChild.children[2];
    while (viewers.firstChild) {
        viewers.removeChild(viewers.firstChild);
    }

    container.appendChild(newPanel);

    return newPanel;
}

function handleViewerPanel(panel, container) {
    let bots = removeBots(panel, container);
    if (bots.length === 0) {
        return;
    }

    if (botsPanel === null) {
        botsPanel = createBotsPanel(panel, container, {ImageSrc: botImageSource, Title: botsPanelTitle, Description: botsPanelDescription});
    }
    let viewers = botsPanel.firstChild.lastChild;
    for (let bot of bots) {
        viewers.appendChild(bot);
    }
}

function handleModeratorPanel(panel, container) {
    if (modBotsBehavior < 0 || modBotsBehavior > 2) {
        alert("[Viewer list bot remover]: modBotsBehavior incorrectly set.");
        return;
    }

    if (modBotsBehavior === 0) {
        return;
    }

    let bots = removeBots(panel, container);
    if (bots.length === 0) {
        return;
    }

    let viewers;
    if (modBotsBehavior === 1) {
        botsPanel = createBotsPanel(panel, container, {ImageSrc: botImageSource, Title: botsPanelTitle, Description: botsPanelDescription});
        viewers = botsPanel.firstChild.lastChild;
    }
    if (modBotsBehavior === 2) {
        let modBotsPanel = createBotsPanel(panel, container, {ImageSrc: modBotImageSource, Title: modBotsPanelTitle, Description: modBotsPanelDescription})
        viewers = modBotsPanel.firstChild.lastChild;
    }

    for (let bot of bots) {
        viewers.insertBefore(bot, viewers.firstChild);
    }


}

async function communityButtonClick() {
    if (viewerListShown) {
        viewerListShown = false;
        return;
    }
    viewerListShown = true;

    var container = await getContainer();
    if (container.children.length === 1) {
        // No one in chat
        return;
    }
    let endScroll = container.lastChild.lastChild.lastChild.lastChild;
    endScroll.remove();

    botsPanel = null;
    for (let i = 1; i < container.children.length; i++) {
        let panel = container.children[i];
        let panelName = panel.firstChild.firstChild.firstChild.children[1].firstChild.firstChild.textContent;
        switch (panelName) {
            case moderatorsPanelName:
                handleModeratorPanel(panel, container);
                break;
            case viewersPanelName:
                handleViewerPanel(panel, container);
                break;
        }
        if (panel.parentElement === null) {
            // Panel was empty, and got removed
            i--;
        }
    }
    container.lastChild.lastChild.lastChild.appendChild(endScroll);
}