NitroType Ban Check (NTL / ntcomps)

Check if a user is banned and update their status

// ==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();

})();