MPP Userscript Core

A library to simplify userscript creation for Multiplayer Piano.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greatest.deepsurf.us/scripts/582107/1849133/MPP%20Userscript%20Core.js

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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