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