MPP Userscript Core

A library to simplify userscript creation for Multiplayer Piano.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/582107/1849133/MPP%20Userscript%20Core.js

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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