Files
BridgeSwarm/native-host/capabilities/discord.js
T
snxraven 693814f46b
CI / Build & Test (push) Failing after 52s
Add Discordjs
2026-09-04 22:10:35 -04:00

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,
};