MPP Userscript Core

A library to simplify userscript creation for Multiplayer Piano.

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/582107/1849133/MPP%20Userscript%20Core.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

/* command registry */
const cmds = {};
function registerCommand(name, func, {...options} = {}) {
    if (!name)
        throw new Error('Command name must be provided.');
    if (typeof name !== 'string')
        throw new TypeError('Command name must be a string.');
    
    if (func == null)
        throw new Error('Command function must be provided.');
    if (typeof func !== 'function');
        throw new TypeError('Command function must be a function.');

    cmds[name] = {
        ...options,
        func
    }
    return cmds[name];
}

/* utilities */
function send(msg) {
    MPP.chat.send(msg);
}
function receive(msg, user = MPP.client.getOwnParticipant(), date = Date.now()) {
    MPP.chat.receive({
        m: 'a',
        a: msg,
        p: user,
        t: date
    });
}
function dm(target, msg) {
    if (target == null)
        throw new Error('Target must be provided.');
    if (typeof target !== 'string')
        throw new TypeError('Target must be a string.');
    if (msg == null)
        throw new Error('Message must be provided.');
    if (typeof msg !== 'string')
        throw new TypeError('Message must be a string.');
        
    MPP.client.sendArray([{
        m: 'dm',
        message: msg,
        _id: target
    }]);
}
function storeItem(key, data) {
    if (!key)
        throw new Error('Item key must be provided.');
    if (typeof key !== 'string')
        throw new TypeError('Item key must be a string.');

    return localStorage[key] = JSON.stringify(data);
}
function readItem(key, fallback = null) {
    if (!key)
        throw new Error('Item key must be provided.');
    if (typeof key !== 'string')
        throw new TypeError('Item key must be a string.');

    return localStorage[key] != null ? JSON.parse(localStorage[key]) : fallback;
}
function findUsers(query) {
    const normalize = text => text.toLowerCase().replace(/[^a-z0-9 -_]+/, '');
    return Object.values(MPP.client.ppl).filter(
        v => normalize(v.name).includes(normalize(query)) ||
             normalize(v._id).includes(normalize(query))
    );
}
function findUser(query) {
    return findUsers(query)?.[0];
}

/* ranks */
const ranks = {
    user: 0
};
const userRanks = readItem('userRanks', {});
function clearUserRank(targetUserID) {
    for (const [name, rank] of Object.entries(ranks)) {
        userRanks[rank] =
            userRanks[rank].filter(userID => userID !== targetUserID);
        if (userRanks[rank].length === 0)
            delete userRanks[rank];
    }
    storeItem('userRanks', userRanks);
}
function getUserRank(userID) {
    let foundRank = -Infinity;
    for (const [name, rank] of Object.entries(ranks)) {
        if (
            userRanks.includes(userID)
            &&
            rank > foundRank
        ) foundRank = rank;
    }
    if (!Number.isFinite(foundRank)) foundRank = ranks.user;
    return foundRank;
}
function setUserRank(userID, rank) {
    // short-circuiting
    if (rank == null)
        throw new Error('Rank ID or name is required.');
    if (typeof rank !== 'number' && typeof rank !== 'string')
        throw new TypeError('Rank must be a string or a number.');

    // coercion
    if (typeof rank === 'string') ranks[rank];
    if (!Object.values(ranks).includes(rank))
        throw new ReferenceError(`Unknown rank ${JSON.stringify(rank)}`);

    clearUserRank(userID);
    userRanks[rank] ??= [];
    userRanks[rank].push(userID);
    storeItem('userRanks', userRanks);
}

/* command handler utils */
function setPrefix(newPrefix) {
    if (prefix == null)
        throw new Error('Prefix must be specified.');
    if (typeof prefix !== 'string')
        throw new TypeError('Prefix must be a string.');

    return prefix = newPrefix;
}
function setPrivate(privacy) {
    if (privacy == null)
        throw new Error('Privacy must be specified.');
    if (typeof prefix !== 'boolean')
        throw new TypeError('Privacy must be a boolean.');

    return private = privacy;
}

/* command handler */
let prefix = '/';
let private = false;
MPP.client.on('a', async event => {
    // event metadata
    const args = event.a.split(' ');
    const user = event.p;
    const userRank = getUserRank(user._id);
    const rawcmd = args[0];
    const fullcmd = rawcmd.toLowerCase();
    const cmd = fullcmd.substring(prefix.length);
    args.shift();

    // handler short-circuit cases
    if (!fullcmd.startsWith(prefix)) return;
    if (private && user._id !== MPP.client.getOwnParticipant()._id) {
        dm(user._id, 'You do not have permission to use this bot.');
        return;
    }
    
    // find command
    let targetCommand = null;
    for (const [name, info] of cmds) {
        if (
            name === cmd
            ||
            info.aliases.includes(cmd)
        ) targetCommand = name;
    }
    
    
    // executor short-circuit cases
    if (targetCommand == null) {
        send(`The command \`${prefix + cmd}\` does not exist.`);
        return;
    }
    if ((targetCommand?.rank ?? ranks.user) > userRank) {
        send('You do not have permission to use this command.');
        return;
    }

    // execute command
    try {
        await targetCommand.func({
            user, args, cmd, fullcmd, rawcmd, prefix, private
        });
    } catch (error) {
        if (Error.isError(error)) {
            const errorLocation = [.../(\d+):(\d+)/.exec(new Error().stack)].slice(1,3);
            const line = errorLocation[0];
            const col = errorLocation[1];
            send(`❌ Uncaught ${error.name} at line ${line} column ${col}: ${error.msg}`);
        } else
            send(`❌ Uncaught RawThrow: ${JSON.stringify(error)}`);
    }
});