Disqualified Status on Flagging Page

Add a column to the flaggings console, with users' disqualified status.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Disqualified Status on Flagging Page
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Add a column to the flaggings console, with users' disqualified status.
// @author       mako640, poem
// @include      https://data.typeracer.com/pit/admin/flagging*
// @grant        GM_xmlhttpRequest
// @connect      data.typeracer.com
// ==/UserScript==

'use strict';

// Remove left margin for less lateral scrolling
document.getElementsByClassName('themeContent')[0].style.marginLeft = 0;

var users_data = []; // Main function will communicate with those waiting for data from other websites using this array
const newColumnTitle = "disqualified";
const displayNameColTitle = "displayName";
const accountDataUrlBase = "https://data.typeracer.com/users?id=tr:";
const historyUrlBase = 'https://data.typeracer.com/pit/race_history?user=';
let displayNameColIndex = null;


function sleep(x) { // Wait for x ms
  return new Promise(resolve => setTimeout(resolve, x));
}

function getElementFromString(tag, string) {
    let element = document.createElement(tag);
    element.innerHTML = string;
    return element;
}

// given a user's display name, return the username
// example: "mako (mako640)" ==> "mako640"
// if it's a guest account, then return null
// note that on the flaggings page, display names always feature parenthesis around usernames, even with blank first and last names
function getUsernameFromDisplayName(displayName) {
    let usernameRegex = /.*\((.*)\)$/;
    let match = usernameRegex.exec(displayName);
    if (match != null) {
        return match[1];
    }
    return null;
}

// add the "Disqualified" table header
function insertDisqualifiedHeader(headerRow) {
    let afterColumn = headerRow.children[displayNameColIndex + 1];
    let newColumn = getElementFromString("th", newColumnTitle);
    headerRow.insertBefore(newColumn, afterColumn);
}

// insert a true/false disqualification value into a table row
function insertDisqualifiedStatus(resultRow, status) {
    let afterColumn = resultRow.children[displayNameColIndex + 1];
    let newColumn = getElementFromString("td", status);
    resultRow.insertBefore(newColumn, afterColumn);
}

function main() {
    // find the results table
    let resultsTableSelector = "table.cellTable:not(.queryTable)";
    let resultsTableList = document.querySelectorAll(resultsTableSelector);
    if (resultsTableList.length === 0) {
        return;
    }

    // find the "displayName" column
    let resultsTable = resultsTableList.item(0);
    let resultsTableRows = resultsTable.getElementsByTagName("tr");
    let resultsTableHeader = resultsTableRows.item(0);
    let resultsTableHeaderCols = resultsTableHeader.children;
    for (let i = 0; i < resultsTableHeaderCols.length; i++) {
        if (resultsTableHeaderCols.item(i).innerText === displayNameColTitle) {
            displayNameColIndex = i;
            insertDisqualifiedHeader(resultsTableHeader); //add "disqualified" label to the first row
        }
    }
    if (displayNameColIndex == null) {
        return;
    }

    // Get each username and call the function to modify their row
    for (let i = 1; i < resultsTableRows.length; i++) {
        let resultRow = resultsTableRows.item(i);
        let displayName = resultRow.children.item(displayNameColIndex).innerText;
        let username = getUsernameFromDisplayName(displayName);
        modifyResultRow(resultRow, username);
    }
}

// given a row of the table, fetch the user's disqualified status for each universe, format this data, and update the row with the formatted data
// is async
async function modifyResultRow(resultRow, username) {
    // if username is null, the user is a guest, and does not have a readable disqualified status
    if (username == null) {
        insertDisqualifiedStatus(resultRow, "N/A");
        return;
    }

    // the functions below will retrieve the user's active universes from their history page, then check the user's disqualified value for each of these universes using the data.typeracer api.
    // These various requests will be performed asynchronously so, while this function is waiting for all of them to be complete, the users_data array will contain an empty string
    // The 'while' loop below will check this value periodically until it's  replaced by the desired data once the requests are complete
    // I have no idea how to properly handle asynchronous functions. Leave me alone.
    users_data.push('');
    let key=users_data.length-1; // Reference of the variable used to communicate in users_data for this particular user (will be propagated to all functions below)

    getUserUniverseStatuses(username,key); //will eventually replace the users_data[key] variable with the desired data

    while(users_data[key]=='')
    {
        await sleep(10);
    }
    let statuses=users_data[key];

    // format the data
    let dqs = 0;
    let nondqs = 0;
    for(let k=0; k<statuses.length; k++)
    {
        let status=statuses[k];
        if(status)
            dqs++;
        else
            nondqs++;
    }
    let formatted_status = '';
    if(dqs>0)
    {
        if(nondqs>0)
            formatted_status = dqs+' true<br>'+nondqs+' false';
        else
            formatted_status='true';
    }
    else formatted_status='false';

    //insert the formatted data
    insertDisqualifiedStatus(resultRow, formatted_status);
}

// Grabs user history html (contains a list of the user's active universes)
function getUserUniverseStatuses(username, key)
{
    let user_history_url = historyUrlBase+username;
	GM_xmlhttpRequest ( {
		method: 'GET',
		url: user_history_url,
		onload: function (response) {
            return getUniverseStatusesFromHTML(username, response.responseText, key);
		}
	});
}

// Extract the universes list from the user's history page html, then for each of those check their disqualified status
function getUniverseStatusesFromHTML(username, html, key)
{
    let universes = [];
    let options = html.split('select name="universe"');
    if(options.length==1)
        universes = ["play"];
    else
    {
        options = options[1].split('\\select')[0].split('option value="');
        let i=1;
        let option;
        while(option=options[i])
        {
            i++;
            let universe=option.split('"')[0] || 'play';
            universes.push(universe);
        }
        if(html.includes('No results matching the given search criteria.'))
            universes.shift();
    }
    let universes_count = universes.length;
    let statuses = [];
    let i=0;
    let universe;
    while(universe=universes[i])
    {
        i++;
        let accountDataUrl = accountDataUrlBase + username + '&universe='+ universe;
        fetch(accountDataUrl)
        .then(response => {
            if (response.status !== 200) {
                console.log('error accessing universe',universe,'status data for user '+username);
                statuses.push('error');
                return;
            }
            response.json().then(data => {
                let isDisqualified = !!(data.tstats && data.tstats.disqualified);
                statuses.push(isDisqualified);

                if(statuses.length==universes_count) // if the disqualified data has been retrieved for each universe, replace the communication variable with the result
                {
                    users_data[key]=statuses;
                    return;
                }
            });
        })
        .catch(err => {
            console.log('error reading universe',universe,'status data for user '+username);
            statuses.push('error');
        });
    }
}

main();