368 lines
11 KiB
JavaScript
368 lines
11 KiB
JavaScript
/**
|
|
* Discord capability pack — real discord.js Client on the Bare host.
|
|
* Page talks to it as BridgeSwarm.DiscordJS.
|
|
*/
|
|
'use strict';
|
|
|
|
const {
|
|
loadBareDiscordJsSync,
|
|
getBareDiscordJsLoadError,
|
|
normalizeDiscordToken,
|
|
} = require('../discord/load-discord.js');
|
|
const {
|
|
intern,
|
|
snapshotValue,
|
|
snapshotEventArgs,
|
|
serializeConstants,
|
|
createHandleTable,
|
|
} = require('../discord/snapshot.js');
|
|
|
|
const MAX_CLIENTS = 4;
|
|
const SKIP_EVENTS = new Set(['raw', 'debug', 'apiRequest', 'apiResponse']);
|
|
const CONSTRUCT_ALLOWED = new Set(['Client', 'REST', 'WebhookClient']);
|
|
|
|
let enabled = false;
|
|
const handles = createHandleTable();
|
|
/** handle -> { names: Set, listener } */
|
|
const listeners = new Map();
|
|
let clientCount = 0;
|
|
|
|
function logErr(msg) {
|
|
try {
|
|
if (process.stderr) process.stderr.write('[bridge-swarm-host] discord: ' + msg + '\n');
|
|
} catch (_) {}
|
|
}
|
|
|
|
function getDiscord() {
|
|
const d = loadBareDiscordJsSync();
|
|
if (!d || typeof d.Client !== 'function') {
|
|
const err = getBareDiscordJsLoadError() || 'bare-discord-js unavailable';
|
|
throw new Error(err);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function decodeArg(arg) {
|
|
if (arg && typeof arg === 'object' && typeof arg._handle === 'string' && handles.byId.has(arg._handle)) {
|
|
return handles.byId.get(arg._handle);
|
|
}
|
|
if (Array.isArray(arg)) return arg.map(decodeArg);
|
|
return arg;
|
|
}
|
|
|
|
function decodeArgs(args) {
|
|
if (!Array.isArray(args)) return [];
|
|
return args.map(decodeArg);
|
|
}
|
|
|
|
function sanitizeConstructArgs(className, args) {
|
|
const a = Array.isArray(args) ? args.map((x) => decodeArg(x)) : [];
|
|
if (className === 'Client' && a[0] && typeof a[0] === 'object') {
|
|
const o = a[0];
|
|
const clean = {};
|
|
if (o.intents != null) clean.intents = o.intents;
|
|
if (o.partials != null) clean.partials = o.partials;
|
|
if (o.failIfNotExists != null) clean.failIfNotExists = o.failIfNotExists;
|
|
if (o.presence && typeof o.presence === 'object') clean.presence = o.presence;
|
|
a[0] = clean;
|
|
}
|
|
if ((className === 'REST' || className === 'WebhookClient') && a[0] && typeof a[0] === 'object') {
|
|
const o = a[0];
|
|
const clean = {};
|
|
if (typeof o.version === 'number' || typeof o.version === 'string') clean.version = o.version;
|
|
if (typeof o.timeout === 'number') clean.timeout = o.timeout;
|
|
if (typeof o.authPrefix === 'string') clean.authPrefix = o.authPrefix;
|
|
a[0] = clean;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function getAtPath(obj, path) {
|
|
const parts = String(path || '').split('.').filter(Boolean);
|
|
let ctx = obj;
|
|
let fn = obj;
|
|
for (let i = 0; i < parts.length; i++) {
|
|
ctx = fn;
|
|
if (fn == null) return { ctx: null, fn: undefined };
|
|
fn = fn[parts[i]];
|
|
}
|
|
return { ctx, fn };
|
|
}
|
|
|
|
function wrapResult(result) {
|
|
if (result == null || typeof result !== 'object') return { value: result };
|
|
if (typeof result.then === 'function') {
|
|
return Promise.resolve(result).then(wrapResult);
|
|
}
|
|
return { value: snapshotValue(result, handles, 0) };
|
|
}
|
|
|
|
function dropHandle(handle) {
|
|
const obj = handles.byId.get(handle);
|
|
const rec = listeners.get(handle);
|
|
if (rec && obj && typeof obj.off === 'function') {
|
|
try {
|
|
obj.off(rec.event || '*');
|
|
} catch (_) {}
|
|
}
|
|
if (rec && rec.unlisten) {
|
|
try {
|
|
rec.unlisten();
|
|
} catch (_) {}
|
|
}
|
|
listeners.delete(handle);
|
|
if (obj) {
|
|
const type = (obj.constructor && obj.constructor.name) || '';
|
|
if (type === 'Client') {
|
|
clientCount = Math.max(0, clientCount - 1);
|
|
if (typeof obj.destroy === 'function') {
|
|
Promise.resolve(obj.destroy()).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
handles.byId.delete(handle);
|
|
handles.meta.delete(handle);
|
|
}
|
|
|
|
function destroyAll() {
|
|
const ids = Array.from(handles.byId.keys());
|
|
for (const id of ids) dropHandle(id);
|
|
clientCount = 0;
|
|
}
|
|
|
|
function refuseDisabled(ctx, allowMeta) {
|
|
if (enabled) return false;
|
|
if (allowMeta) return false;
|
|
ctx.reply({ ok: false, error: 'Discord is disabled. Enable it in BridgeSwarm Settings.' });
|
|
return true;
|
|
}
|
|
|
|
function createDiscordPack() {
|
|
const commands = {
|
|
async status(ctx) {
|
|
let loaded = false;
|
|
let loadError = '';
|
|
try {
|
|
const d = loadBareDiscordJsSync();
|
|
loaded = !!(d && typeof d.Client === 'function');
|
|
if (!loaded) loadError = getBareDiscordJsLoadError();
|
|
} catch (err) {
|
|
loadError = err && err.message ? err.message : String(err);
|
|
}
|
|
ctx.reply({
|
|
ok: true,
|
|
enabled,
|
|
loaded,
|
|
loadError: loadError || undefined,
|
|
clients: clientCount,
|
|
maxClients: MAX_CLIENTS,
|
|
});
|
|
},
|
|
|
|
async setEnabled(ctx) {
|
|
const on = !!(ctx.payload && ctx.payload.enabled === true);
|
|
enabled = on;
|
|
if (!on) destroyAll();
|
|
ctx.reply({ ok: true, enabled, clients: clientCount });
|
|
},
|
|
|
|
async surface(ctx) {
|
|
try {
|
|
const discord = getDiscord();
|
|
ctx.reply({
|
|
ok: true,
|
|
enabled,
|
|
loaded: true,
|
|
surface: serializeConstants(discord),
|
|
});
|
|
} catch (err) {
|
|
ctx.reply({
|
|
ok: false,
|
|
error: err.message,
|
|
enabled,
|
|
loaded: false,
|
|
loadError: getBareDiscordJsLoadError() || err.message,
|
|
});
|
|
}
|
|
},
|
|
|
|
async construct(ctx) {
|
|
if (refuseDisabled(ctx)) return;
|
|
try {
|
|
const discord = getDiscord();
|
|
const className = String((ctx.payload && ctx.payload.className) || 'Client');
|
|
if (!CONSTRUCT_ALLOWED.has(className)) {
|
|
ctx.reply({ ok: false, error: 'class not allowed: ' + className });
|
|
return;
|
|
}
|
|
const Ctor = discord[className];
|
|
if (typeof Ctor !== 'function') {
|
|
ctx.reply({ ok: false, error: className + ' is not available' });
|
|
return;
|
|
}
|
|
if (className === 'Client' && clientCount >= MAX_CLIENTS) {
|
|
ctx.reply({ ok: false, error: 'too many Discord clients (max ' + MAX_CLIENTS + ')' });
|
|
return;
|
|
}
|
|
const args = sanitizeConstructArgs(className, (ctx.payload && ctx.payload.args) || []);
|
|
const inst = new Ctor(...args);
|
|
const handle = intern(handles, inst);
|
|
if (className === 'Client') clientCount += 1;
|
|
handles.meta.get(handle).origin = ctx.payload && ctx.payload._origin;
|
|
ctx.reply({
|
|
ok: true,
|
|
handle,
|
|
_type: className,
|
|
_methods: className === 'Client' ? ['login', 'destroy'] : ['setToken', 'put', 'post', 'get', 'patch', 'delete'],
|
|
});
|
|
} catch (err) {
|
|
ctx.reply({ ok: false, error: err.message });
|
|
}
|
|
},
|
|
|
|
async call(ctx) {
|
|
if (refuseDisabled(ctx)) return;
|
|
try {
|
|
const handle = ctx.payload && ctx.payload.handle;
|
|
const path = (ctx.payload && ctx.payload.path) || '';
|
|
const obj = handles.byId.get(handle);
|
|
if (!obj) {
|
|
ctx.reply({ ok: false, error: 'unknown handle' });
|
|
return;
|
|
}
|
|
let args = decodeArgs((ctx.payload && ctx.payload.args) || []);
|
|
if (path === 'login' || path === 'setToken') {
|
|
if (typeof args[0] === 'string') args[0] = normalizeDiscordToken(args[0]);
|
|
}
|
|
const { ctx: recv, fn } = getAtPath(obj, path);
|
|
if (typeof fn !== 'function') {
|
|
ctx.reply({ ok: true, value: snapshotValue(fn, handles, 0) });
|
|
return;
|
|
}
|
|
const result = await wrapResult(fn.apply(recv, args));
|
|
ctx.reply(Object.assign({ ok: true }, result));
|
|
} catch (err) {
|
|
ctx.reply({ ok: false, error: err.message });
|
|
}
|
|
},
|
|
|
|
async get(ctx) {
|
|
if (refuseDisabled(ctx)) return;
|
|
try {
|
|
const handle = ctx.payload && ctx.payload.handle;
|
|
const path = (ctx.payload && ctx.payload.path) || '';
|
|
const obj = handles.byId.get(handle);
|
|
if (!obj) {
|
|
ctx.reply({ ok: false, error: 'unknown handle' });
|
|
return;
|
|
}
|
|
const { fn } = getAtPath(obj, path);
|
|
ctx.reply({ ok: true, value: snapshotValue(fn, handles, 0) });
|
|
} catch (err) {
|
|
ctx.reply({ ok: false, error: err.message });
|
|
}
|
|
},
|
|
|
|
async listen(ctx) {
|
|
if (refuseDisabled(ctx)) return;
|
|
const handle = ctx.payload && ctx.payload.handle;
|
|
const obj = handles.byId.get(handle);
|
|
if (!obj || typeof obj.on !== 'function') {
|
|
ctx.reply({ ok: false, error: 'handle is not an EventEmitter' });
|
|
return;
|
|
}
|
|
if (listeners.has(handle)) {
|
|
ctx.reply({ ok: true, handle, listening: true });
|
|
return;
|
|
}
|
|
const emit = ctx.emit;
|
|
const includeDebug = !!(ctx.payload && ctx.payload.debug);
|
|
const listener = function (eventName) {
|
|
if (!includeDebug && SKIP_EVENTS.has(String(eventName))) return;
|
|
const args = Array.prototype.slice.call(arguments, 1);
|
|
let payload;
|
|
try {
|
|
payload = snapshotEventArgs(args, handles);
|
|
} catch (err) {
|
|
payload = [{ _type: 'Error', message: err.message }];
|
|
}
|
|
try {
|
|
emit('cap-chunk', {
|
|
pack: 'discord',
|
|
kind: 'event',
|
|
handle,
|
|
name: eventName,
|
|
args: payload,
|
|
});
|
|
} catch (err) {
|
|
logErr('emit failed: ' + (err && err.message));
|
|
}
|
|
};
|
|
obj.on('error', function (err) {
|
|
listener('error', err);
|
|
});
|
|
if (typeof obj.on === 'function') {
|
|
obj.on('*', function () {});
|
|
}
|
|
const origEmit = obj.emit;
|
|
if (typeof origEmit === 'function') {
|
|
obj.emit = function (eventName) {
|
|
try {
|
|
listener.apply(null, arguments);
|
|
} catch (_) {}
|
|
return origEmit.apply(obj, arguments);
|
|
};
|
|
}
|
|
listeners.set(handle, {
|
|
unlisten() {
|
|
if (typeof origEmit === 'function') obj.emit = origEmit;
|
|
},
|
|
});
|
|
ctx.reply({ ok: true, handle, listening: true });
|
|
},
|
|
|
|
async destroy(ctx) {
|
|
const handle = ctx.payload && ctx.payload.handle;
|
|
if (handle) {
|
|
dropHandle(handle);
|
|
ctx.reply({ ok: true, handle });
|
|
return;
|
|
}
|
|
destroyAll();
|
|
ctx.reply({ ok: true, clients: 0 });
|
|
},
|
|
};
|
|
|
|
return {
|
|
id: 'discord',
|
|
commands,
|
|
getPublicStatus() {
|
|
let loaded = false;
|
|
try {
|
|
const d = loadBareDiscordJsSync();
|
|
loaded = !!(d && typeof d.Client === 'function');
|
|
} catch (_) {}
|
|
return {
|
|
installed: true,
|
|
enabled,
|
|
clients: clientCount,
|
|
maxClients: MAX_CLIENTS,
|
|
loaded,
|
|
loadError: getBareDiscordJsLoadError() || undefined,
|
|
};
|
|
},
|
|
onLoad() {},
|
|
cleanup() {
|
|
destroyAll();
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createDiscordPack,
|
|
normalizeDiscordToken,
|
|
serializeConstants: require('../discord/snapshot.js').serializeConstants,
|
|
snapshotValue: require('../discord/snapshot.js').snapshotValue,
|
|
createHandleTable: require('../discord/snapshot.js').createHandleTable,
|
|
};
|