Racing Skill Display

Shows the racing skill of drivers in your race as long as they have this script installed as well

// ==UserScript==
// @name         Racing Skill Display
// @namespace    https://github.com/zim312/raceskill
// @version      0.0.1
// @description  Shows the racing skill of drivers in your race as long as they have this script installed as well
// @originalauthor       Sulsay [2173590]
// @author	 Zim312 [1984387]
// @match        https://www.torn.com/loader.php?sid=racing*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

const arsonBaseApiUrl = 'https://cs.etmc.org/torn/api/v1';
const racingSkillCacheByDriverId = new Map();

(async function () {
    const racingSkillElm = document.querySelector('.skill');
    await saveRacingSkill(getUserIdFromCookie(), racingSkillElm.innerText);

    insertRacingSkillsIntoCurrentDriversList();

    // On change race tab, (re-)insert the racing skills if applicable:
    new MutationObserver(insertRacingSkillsIntoCurrentDriversList).observe(document.getElementById('racingAdditionalContainer'), {childList: true});
})();

async function insertRacingSkillsIntoCurrentDriversList() {
    const driversList = document.getElementById('leaderBoard');
    if (driversList === null) {
        return;
    }

    watchForDriversListContentChanges(driversList);

    const racingSkills = await getRacingSkillForDrivers(getDriverIds(driversList));
    for (let driver of driversList.querySelectorAll('.driver-item')) {
        const driverId = getDriverId(driver);
        if (! racingSkills[driverId]) {
            continue;
        }
        const nameDiv = driver.querySelector('.name');
        nameDiv.style.position = 'relative';
        nameDiv.insertAdjacentHTML('beforeend', `<span style="position:absolute;right:5px">${racingSkills[driverId]}</span>`);
    }
}

function watchForDriversListContentChanges(driversList) {
    if (driversList.dataset.hasWatcher !== undefined) {
        return;
    }

    // The content of #leaderBoard is rebuilt periodically so watch for changes:
    new MutationObserver(insertRacingSkillsIntoCurrentDriversList).observe(driversList, {childList: true});
    driversList.dataset.hasWatcher = 'true';
}

function getDriverIds(driversList) {
    return Array.from(driversList.querySelectorAll('.driver-item')).map(driver => getDriverId(driver));
}

function getDriverId(driverUl) {
    return +driverUl.closest('li').id.substr(4);
}

async function getRacingSkillForDrivers(driverIds) {
    const driverIdsToFetchSkillFor = driverIds.filter(driverId => ! racingSkillCacheByDriverId.has(driverId));
    if (driverIdsToFetchSkillFor.length > 0) {
        const skills = await fetchRacingSkillForDrivers(driverIdsToFetchSkillFor);
        for (let [driverId, skill] of Object.entries(skills)) {
            racingSkillCacheByDriverId.set(+driverId, skill);
        }
    }

    const resultHash = {};
    for (let driverId of driverIds) {
        resultHash[driverId] = racingSkillCacheByDriverId.get(driverId);
    }
    return resultHash;
}

function getUserIdFromCookie() {
    const userIdString = document.cookie.split(';')
        .map(entry => entry.trim())
        .find(entry => entry.indexOf('uid=') === 0)
        .replace('uid=', '');

    return parseInt(userIdString, 10);
}

function fetchRacingSkillForDrivers(driverIds) {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: `${arsonBaseApiUrl}/racing-skills?expect_strings&drivers=${driverIds.join(',')}`,
            onload: ({responseText}) => resolve(JSON.parse(responseText)),
        });
    });
}

function saveRacingSkill(userId, racingSkillString) {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${arsonBaseApiUrl}/players/${userId}/racing-skill`,
            data: JSON.stringify({racing_skill: racingSkillString}),
            headers: {'Content-Type': 'application/json'},
            onload: resolve,
        });
    });
}