Mostrar Notas

Script para mostrar las notas actuales de todos los usuarios en el scoreboard del CTFd. (TODO: Manejo de errores en las requests)

// ==UserScript==
// @name         Mostrar Notas
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      0.1.551
// @description  Script para mostrar las notas actuales de todos los usuarios en el scoreboard del CTFd. (TODO: Manejo de errores en las requests)
// @author       Neftalí Toledo
// @match        https://ic.catedras.linti.unlp.edu.ar/scoreboard
// @icon         https://www.google.com/s2/favicons?sz=64&domain=edu.ar
// @grant        none
// @run-at       document-end
// ==/UserScript==

(async function() {
    'use strict';
    
    const ENDPOINT = "https://ic.catedras.linti.unlp.edu.ar/api/v1/users/USER_ID/solves";
    const RETOS_ENDPOINT = "https://ic.catedras.linti.unlp.edu.ar/api/v1/challenges"

    // Define los valores de suma correspondientes a cada categoría para el promedio ponderado
    //const cantidad_dificultades = 3; (SIN USO)
    const valorEasy = 4;
    const valorMedium = 3;
    const valorHard = 3;

    const valoresTotales = await obtenerValoresTotales(); //Obtiene los totales para calcular el promedio

    // Categorias que no se van a tener en cuenta para el calculo de la nota
    const categoriasExcluidas = [
        "Extras-(no-suman-nota)",
        "Practica-0_Scripting"
    ]

    //Promedio

    function calcularPromedioPonderado(cantidadEasy, cantidadMedium, cantidadHard, categoria) {
        // Calcula la suma ponderada
        const promedioEasy = calcularPromedio(cantidadEasy, valoresTotales[categoria]["Easy"] || 1);
        const promedioMedium = calcularPromedio(cantidadMedium, valoresTotales[categoria]["Medium"] || 1);
        const promedioHard = calcularPromedio(cantidadHard, valoresTotales[categoria]["Hard"] || 1);

        return (promedioEasy * valorEasy + promedioMedium * valorMedium + promedioHard * valorHard);
    }

    function calcularPromedio(retos_resueltos, total) {
        return (retos_resueltos / total);
    }

    // Filtros

    function filtrarDesafios(data, total = false) {
        const result = {};
    
        data.forEach(item => {
            const categoryKey = total ? item.category : item.challenge.category;
            const [categoria, dificultad] = categoryKey.split(" - ");
    
            result[categoria] = result[categoria] || {};
            result[categoria][dificultad] = (result[categoria][dificultad] || 0) + 1;
        });
    
        return result;
    }
    
    // Exclusiones

    function excluirCategorias(categorias) {
        const categoriasExcluidasSet = new Set(categoriasExcluidas); // Se convierte en Set por eficiencia para muchas categorías excluidas
        return categorias.filter(categoria => !categoriasExcluidasSet.has(categoria));
    }

    // Obtener la cantidad de retos resueltos por el usuario

    async function obtenerRetosResueltos(user) {
        const req = ENDPOINT.replace("USER_ID", user);
        const response = await fetch(req);
        const dataJSON = await response.json();
        
        return dataJSON;
    }

    // Obtener nota para cada practica

    function obtenerNotas(data) {
        const retos_resueltos = filtrarDesafios(data);
        const categorias = excluirCategorias(Object.keys(retos_resueltos));
        
        const notas = categorias
            .filter(categoria => Object.keys(retos_resueltos[categoria]).length > 0)
            .map(categoria => {
                const cantidadEasy = retos_resueltos[categoria]["Easy"] || 0;
                const cantidadMedium = retos_resueltos[categoria]["Medium"] || 0;
                const cantidadHard = retos_resueltos[categoria]["Hard"] || 0;
                
                const promedioPonderado = calcularPromedioPonderado(cantidadEasy, cantidadMedium, cantidadHard, categoria);
                return `${categoria}: ${parseFloat(promedioPonderado).toPrecision(3)}`;
            })
            .reverse().join("<br>");
    
        return notas;
    }

    // Obtener el total de retos por categoría
    async function obtenerValoresTotales(){
        const response = await fetch(RETOS_ENDPOINT);
        const retosJSON = await response.json();
        const retos_total = filtrarDesafios(retosJSON.data, true);
        return retos_total;
    }

    // Obtenemos la tabla de usuarios y sus filas
    const tablaUsuarios = document.querySelector(".table-striped");
    const filas = tablaUsuarios.querySelectorAll("tr");

    // Añadir columna de notas
    const columnaNotas = document.createElement("td");
    columnaNotas.setAttribute("scope", "col");
    columnaNotas.innerHTML = "<b>Notas</b>";
    filas[0].appendChild(columnaNotas);

    // Obtener las notas de cada usuario
    const userPromises = [];
    for (let i = 1; i < filas.length; i++) {
        //A veces este link cambia a "https://ic.catedras.linti.unlp.edu.ar/teams/" en vez de "https://ic.catedras.linti.unlp.edu.ar/users/"
        // No se por qué pasa
        const userID = parseInt(filas[i].querySelector("td a").href.replace("https://ic.catedras.linti.unlp.edu.ar/users/", ""));
        userPromises.push(obtenerRetosResueltos(userID));
    }

    // Esperar a que se resuelvan todas las promesas
    const userNotes = await Promise.all(userPromises);

    // Construir notas como una cadena HTML
    const notasHTML = userNotes.map(userNote => {
        const notas = obtenerNotas(userNote.data);
        return notas || "No resolvió ningún reto";
    });

    // Añadir notas a cada fila
    for (let i = 1; i < filas.length; i++) {
        const elementoNota = document.createElement("td");
        elementoNota.innerHTML = notasHTML[i - 1];
        filas[i].appendChild(elementoNota);
    }
})();