MPP Userscript Core

A library to simplify userscript creation for Multiplayer Piano.

Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.greasyfork.org/scripts/582107/1849133/MPP%20Userscript%20Core.js

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

/* 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)}`);
    }
});