NitroType Ban Check (NTL / ntcomps)

Check if a user is banned and update their status

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NitroType Ban Check (NTL / ntcomps)
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Check if a user is banned and update their status
// @match        https://www.nitrotype.com/*
// @grant        GM_xmlhttpRequest
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fontawesome.com/icons/ban
// @connect      ntleaderboards.onrender.com
// @connect      ntcomps.com
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    let NT_COMPS_TOKEN;
    const NT_TOKEN = `Bearer ${localStorage.getItem("player_token")}`;
    const VALID_PAGE_PATTERNS = [{
            pattern: "https://www.nitrotype.com/leagues",
            handler: global_handleLeaguesPage
        },
        {
            pattern: "https://www.nitrotype.com/racer/",
            handler: global_handleUserPage
        },
        {
            pattern: "https://www.nitrotype.com/team/",
            handler: global_handleTeamPage
        },
    ];

    function init() {
        console.log("[BAN_CHECK_LOG] - Init. Checking page type.");
        const location = window.location.href;
        const validPage = VALID_PAGE_PATTERNS.find(({
            pattern
        }) => location.startsWith(pattern));

        if (validPage) {
            console.log("[BAN_CHECK_LOG]] - Valid page, getting NTComps Token.");
            get_NTCOMPS_TOKEN().then(token => {
                NT_COMPS_TOKEN = token.replace(/\n/g, '');
                main(validPage.handler);
            }).catch(error => {
                console.error(error);
            });
        } else {
            console.log("[BAN_CHECK_LOG]] - Invalid page, skipping token retrieval.");
        }
    }

    function main(handler) {
        console.log("[BAN_CHECK_LOG]] - Token updated, handling page.");
        handler();
    }

    function global_handleUserPage() {
        console.log("[BAN_CHECK_LOG]] - Handling User Page.");
        const username = getUsernameFromUrl();
        if (username) {
            getStatusAndColor(username)
                .then(({
                    finalStatus,
                    color
                }) => {
                    console.log(`[BAN_CHECK_LOG] - Status for user ${username} determined as ${finalStatus}, updating.`);
                    updateProfileStatus(finalStatus, color);
                })
                .catch(error => {
                    console.error("Error processing user:", error);
                });
        }

        function updateProfileStatus(finalStatus, color) {
            const playerNameContainer = document.querySelector('.profile-title');
            const statusLabel = document.createElement('span');
            statusLabel.textContent = finalStatus;
            statusLabel.style.color = color;
            statusLabel.style.marginLeft = "10px";
            if (playerNameContainer) {
                playerNameContainer.appendChild(statusLabel);
            }
        }

        function getUsernameFromUrl() {
            const pathParts = window.location.pathname.split('/');
            return pathParts[pathParts.length - 1];
        }
    }

    function global_handleTeamPage() {
        console.log(`[BAN_CHECK_LOG] - Handling Team page`);
        let countdown = 1;

        const countdownInterval = setInterval(() => {
            countdown--;

            if (countdown < 0) {
                clearInterval(countdownInterval);

                checkUserBansTeam();
            }
        }, 1000);


        async function updateUsersTeamApplications(userMap) {
            for (const [displayName, username] of Object.entries(userMap)) {
                try {
                    const {
                        finalStatus,
                        color
                    } = await getStatusAndColor(username);
                    console.log(`[BAN_CHECK_LOG] - Status for user ${username} determined as ${finalStatus}, updating.`);
                    await updateUserStatusTeamApplications(finalStatus, color, username, displayName);
                } catch (error) {
                    console.error(`Error processing user ${username}:`, error);
                }
            }
        }
        async function updateUsersTeam(userMap) {
            for (const [displayName, username] of Object.entries(userMap)) {
                try {
                    const {
                        finalStatus,
                        color
                    } = await getStatusAndColor(username);
                    console.log(`[BAN_CHECK_LOG] - Status for user ${username} determined as ${finalStatus}, updating.`);
                    await updateUserStatusTeam(finalStatus, color, username, displayName);
                } catch (error) {
                    console.error(`Error processing user ${username}:`, error);
                }
            }
        }


        async function checkUserBansTeam() {
            const applicationsMap = await fetchTeamApplications();
            const userMap = await fetchTeamActivity();
            document.querySelector('.table-cell.table-cell--races.table-filter').click();
            if (applicationsMap){
                console.log(`[BAN_CHECK_LOG] - Checking Team Applications`);
                updateUsersTeamApplications(applicationsMap);
            }
            if (userMap) {
                console.log(`[BAN_CHECK_LOG] - User Activity Retrieved, updating users`);
                updateUsersTeam(userMap);
            } else {
                console.error("Failed to retrieve user map.");
            }


        }

        function updateUserStatusTeamApplications(finalStatus, color, username, displayName) {
            //console.log("Checking team applications", finalStatus, color, username, displayName);
            const team_table = document.querySelector(".table.table--a.table--striped.well.well--m.well--b");
            const playerNameContainers = team_table.querySelectorAll('.player-name--container[title]');
            const playerNameContainer = Array.from(playerNameContainers).find(container => {
                const nameSpan = container.querySelector('.type-ellip');
                const isNameMatch = nameSpan && nameSpan.textContent.trim() === displayName.trim();

                if (isNameMatch) {
                    return container;
                }

                return false;
            });
            //console.log(playerNameContainer);
            const parentCont = playerNameContainer.parentElement;
            const titleCont = parentCont.nextElementSibling;
            const statusLabel = document.createElement('span');
            statusLabel.textContent = finalStatus;
            statusLabel.style.color = color;
            statusLabel.style.fontWeight = "bold";
            const existingStatusLabel = playerNameContainer.querySelector('.status-label');
            if (existingStatusLabel) {
                existingStatusLabel.remove();
            }
            if (titleCont) {
                statusLabel.classList.add('status-label');
                titleCont.innerHTML = '';
                titleCont.appendChild(statusLabel);
            }


        }

        function updateUserStatusTeam(finalStatus, color, username, displayName) {
            const team_table = document.querySelector('.table.table--striped.table--selectable.table--team.table--teamOverview');
            const playerNameContainers = team_table.querySelectorAll('.player-name--container[title]');
            const playerNameContainer = Array.from(playerNameContainers).find(container => {
                const nameSpan = container.querySelector('.type-ellip');
                const isNameMatch = nameSpan && nameSpan.textContent.trim() === displayName.trim();

                if (isNameMatch) {
                    return container;
                }

                return false;
            });
            //console.log(playerNameContainer);
            const parentCont = playerNameContainer.parentElement;
            const titleCont = parentCont.nextElementSibling;
            const statusLabel = document.createElement('span');
            statusLabel.textContent = finalStatus;
            statusLabel.style.color = color;
            statusLabel.style.fontWeight = "bold";
            const existingStatusLabel = playerNameContainer.querySelector('.status-label');
            if (existingStatusLabel) {
                existingStatusLabel.remove();
            }
            if (titleCont) {
                statusLabel.classList.add('status-label');
                titleCont.innerHTML = '';
                titleCont.appendChild(statusLabel);
            }

        }
        async function fetchTeamApplications() {
            try {
                const response = await fetch("https://www.nitrotype.com/api/v2/teams/applications", {
                    headers: {
                        accept: "application/json, text/plain, */*",
                        authorization: NT_TOKEN,
                        "sec-fetch-mode": "cors",
                        "sec-fetch-site": "same-origin",
                    },
                    referrer: "https://www.nitrotype.com/team/FASZ",
                    referrerPolicy: "same-origin",
                    method: "GET",
                    mode: "cors",
                    credentials: "include"
                });

                const data = await response.json(); // Convert the response to JSON
                const memberMap = {};

                // Populate memberMap based on data.results
                data.results.forEach(member => {
                    const { displayName, username } = member;
                    memberMap[displayName || username] = username;
                });

                console.log(memberMap); // Log memberMap for inspection
                return memberMap; // Return the populated memberMap
            } catch (error) {
                console.error("Error fetching team applications:", error);
                return null;
            }
        }


        async function fetchTeamActivity() {
            try {
                const TEAM = window.location.pathname.split('/').pop();
                const response = await fetch(`https://www.nitrotype.com/api/v2/teams/${TEAM}`, {
                    headers: {
                        accept: "application/json, text/plain, */*",
                        authorization: NT_TOKEN,
                    },
                    referrer: `https://www.nitrotype.com/team/${TEAM}`,
                    referrerPolicy: "same-origin",
                    method: "GET",
                    mode: "cors",
                    credentials: "include"
                });

                const data = await response.json();
                if (data.status === "OK") {
                    const members = data.results.members;
                    members.sort((a, b) => b.played - a.played);

                    const memberMap = {};

                    members.forEach(member => {
                        const {
                            displayName,
                            username
                        } = member;
                        memberMap[displayName || username] = username;
                    });
                    console.log(memberMap);
                    return memberMap;
                } else {
                    console.error("Error: ", data.status);
                    return null;
                }
            } catch (error) {
                console.error("Fetch error: ", error);
                return null;
            }
        }


    }

    function global_handleLeaguesPage() {
        console.log(`[BAN_CHECK_LOG] - Handling Leagues page`);
        let countdown = 1;

        const countdownInterval = setInterval(() => {
            countdown--;

            if (countdown < 0) {
                clearInterval(countdownInterval);
                checkUserBans();
            }
        }, 1000);
        async function fetchUserActivity() {
            try {
                const response = await fetch("https://www.nitrotype.com/api/v2/leagues/user/activity", {
                    headers: {
                        accept: "application/json, text/plain, */*",
                        authorization: NT_TOKEN,
                    },
                    referrer: "https://www.nitrotype.com/leagues",
                    referrerPolicy: "same-origin",
                    method: "GET",
                    mode: "cors",
                    credentials: "include"
                });

                const data = await response.json();
                if (data.status === "OK") {
                    const standings = data.results.standings;
                    standings.sort((a, b) => b.experience - a.experience);

                    const userMap = {};

                    standings.forEach(user => {
                        const {
                            displayName,
                            username
                        } = user;
                        userMap[displayName || username] = username;
                    });
                    return userMap;
                } else {
                    console.error("Error: ", data.status);
                    return null;
                }
            } catch (error) {
                console.error("Fetch error: ", error);
                return null;
            }
        }
        async function updateUsers(userMap) {
            for (const [displayName, username] of Object.entries(userMap)) {
                try {
                    const {
                        finalStatus,
                        color
                    } = await getStatusAndColor(username);
                    console.log(`[BAN_CHECK_LOG] - Status for user ${username} determined as ${finalStatus}, updating.`);
                    await updateUserStatus(finalStatus, color, username, displayName);
                } catch (error) {
                    console.error(`Error processing user ${username}:`, error);
                }
            }
        }


        async function checkUserBans() {

            const userMap = await fetchUserActivity();

            if (userMap) {
                console.log(`[BAN_CHECK_LOG] - User Activity Retrieved, updating users`);
                updateUsers(userMap);
            } else {
                console.error("Failed to retrieve user map.");
            }

        }

        function updateUserStatus(finalStatus, color, username, displayName) {
            const playerNameContainers = document.querySelectorAll('.player-name--container[title]');
            const playerNameContainer = Array.from(playerNameContainers).find(container => {
                const nameSpan = container.querySelector('.type-ellip');
                const isNameMatch = nameSpan && nameSpan.textContent.trim() === displayName.trim();

                if (isNameMatch) {
                    return container;
                }

                return false;
            });
            const parentCont = playerNameContainer.parentElement;
            const titleCont = parentCont.nextElementSibling;
            const statusLabel = document.createElement('span');
            statusLabel.textContent = finalStatus;
            statusLabel.style.color = color;
            statusLabel.style.fontWeight = "bold";
            const existingStatusLabel = playerNameContainer.querySelector('.status-label');
            if (existingStatusLabel) {
                existingStatusLabel.remove();
            }
            if (titleCont) {
                statusLabel.classList.add('status-label');
                titleCont.innerHTML = '';
                titleCont.appendChild(statusLabel);
            }

        }
    }


    function get_NTCOMPS_TOKEN() {
        const targetUrl = 'https://www.ntcomps.com/racers/search';
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                headers: {
                    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                url: targetUrl,
                onload: function(response) {
                    if (response.status === 200) {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');
                        const tokenElement = doc.querySelector('input[name="authenticity_token"]');
                        if (tokenElement) {
                            const tokenValue = tokenElement.value;
                            resolve(tokenValue);
                        } else {
                            reject('authenticity_token element not found.');
                        }
                    } else {
                        reject(`Failed to fetch page: ${response.status}`);
                    }
                },
                onerror: function(error) {
                    reject('Error fetching the page: ' + error);
                }
            });
        });
    }

    async function getStatusAndColor(username, retries = 3, delay = 1000) {
        let attempts = 0;

        while (attempts < retries) {
            try {
                const NTL_status = await ntleaderboards_check(username);
                const NTC_status = await ntcomps_check(username);
                //console.log(NTL_status, NTC_status);
                const {
                    finalStatus,
                    color
                } = determineFinalStatus(NTL_status, NTC_status);

                return {
                    finalStatus,
                    color
                };
            } catch (error) {
                attempts++;
                console.error(`Attempt ${attempts} failed:`, error);

                if (attempts < retries) {
                    console.log(`Retrying in ${delay}ms...`);
                    await new Promise(resolve => setTimeout(resolve, delay));
                } else {
                    console.error("Max retries reached. Throwing error.");
                    throw error;
                }
            }
        }
    }

    // Function to check if a user is banned from ntleaderboards
    // return "Not Banned" (legit or not found) / Banned" (and flagged) / "Banned, but not bot" (if banned but no flag)
    function ntleaderboards_check(username) {
        console.log(`[BAN_CHECK_LOG] - Waiting for ${username} status from ntleaderboards.`);
        const url = `https://ntleaderboards.onrender.com/is_user_banned/${username}`;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        const data = response.responseText;
                        let banned_status;
                        if (data === "N") {
                            banned_status = "Not Banned";
                        } else {
                            if (data.includes("flag")) {
                                banned_status = "Banned";
                            } else {
                                banned_status = "Banned, but not bot";
                            }
                        }
                        resolve(banned_status);
                    } else {
                        reject(`Request failed with status: ${response.status}`);
                    }
                },
                onerror: function(error) {
                    reject('Error fetching data: ' + error);
                }
            });
        });
    }

    // Function to check racer status from ntcomps // Return "Flagged" or "Legit" or "Not found"
    async function ntcomps_check(searchString) {
        console.log(`[BAN_CHECK_LOG] - Waiting for ${searchString} status from ntcomps.`);
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: "https://www.ntcomps.com/racers/search",
                headers: {
                    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Sec-Fetch-Dest": "document",
                    "Sec-Fetch-Mode": "navigate",
                    "Sec-Fetch-Site": "same-origin",
                },
                data: `authenticity_token=${encodeURIComponent(NT_COMPS_TOKEN)}&racer%5Bsearch_string%5D=${encodeURIComponent(searchString)}&racer%5Bflagged%5D=0&commit=Search+racer`,
                onload: function(response) {
                    if (response.status === 200) {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const rows = doc.querySelectorAll("tbody tr");

                        for (const row of rows) {
                            const cells = row.querySelectorAll("td");
                            if (cells.length >= 6) {
                                const racerName = cells[2].textContent.trim();

                                if (racerName.toLowerCase() === searchString.toLowerCase()) {
                                    const status = cells[5].textContent.trim();
                                    resolve(status);
                                    return;
                                }
                            }
                        }
                        resolve("Not found");
                    } else {
                        console.error('Request failed with status:', response.status);
                        reject("Error");
                    }
                },
                onerror: function(error) {
                    console.error('Error occurred:', error);
                    reject("Error");
                }
            });
        });
    }

    //Possible Final Statuses
    //"Bot (100%)" if Banned / Flagged at both - RED
    //"Bot (ntcomps)" if Flagged at ntcomps only - RED
    //"Bot (NTL)" if Flagged at NTL only - RED
    //"NTL banned (not bot?)" - When banned at NTL only, but not for botting - ORANGE
    //"Legit" - Not banned, not Flagged at both - GREEN
    //GRAY color should mean ERROR
    function determineFinalStatus(NTL_status, NTC_status) {
        console.log({"NTL":NTL_status, "NTC": NTC_status});
        const statusMap = {
            "Not Banned": {
                "Flagged on NTcomps": {
                    finalStatus: "Bot (ntcomps)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on both platforms": {
                    finalStatus: "Bot (100%)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on NTLeaderboards": {
                    finalStatus: "Bot (NTL)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Legit": {
                    finalStatus: "Legit",
                    color: "rgb(0, 255, 0)"
                }, // Green
                "Not found": {
                    finalStatus: "Unknown player",
                    color: "rgb(255, 255, 0)"
                } // Yellow
            },
            "Banned": {
                "Flagged on NTcomps": {
                    finalStatus: "Bot (ntcomps)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on both platforms": {
                    finalStatus: "Bot (100%)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on NTLeaderboards": {
                    finalStatus: "Bot (NTL)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Legit": {
                    finalStatus: "Bot (NTL)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Not found": {
                    finalStatus: "Bot (NTL)",
                    color: "rgb(255, 0, 0)"
                } // Red
            },
            "Banned, but not bot": {
                "Flagged on NTcomps": {
                    finalStatus: "Bot (ntcomps)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on both platforms": {
                    finalStatus: "Bot (100%)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Flagged on NTLeaderboards": {
                    finalStatus: "Bot (NTL)",
                    color: "rgb(255, 0, 0)"
                }, // Red
                "Legit": {
                    finalStatus: "NTL banned (not bot?)",
                    color: "rgb(255, 165, 0)"
                }, // Orange
                "Not found": {
                    finalStatus: "NTL banned (not bot?)",
                    color: "rgb(255, 165, 0)"
                } // Orange
            }
        };
        const result = statusMap[NTL_status]?.[NTC_status];
        return result || {
            finalStatus: "Unknown status",
            color: "rgb(128, 128, 128)"
        }; // Default to gray color for unknown cases
    }

    init();

})();