// Tiny fetch wrapper for calling the bot process's /internal/* API (shared // secret, same pattern as requireInternalKey on both sides). Used by the // Discord Bot admin controller to push config after a save and to poll live // status for the admin panel. Never throws — callers get { ok: false, error } // on any failure (bot unreachable, timeout, non-2xx) so an admin save/poll // never 500s just because the bot container is down or restarting. const log = require('./logger')('bot-internal-client') const BASE_URL = process.env.BOT_INTERNAL_URL || 'http://localhost:4100' const KEY = process.env.BOT_INTERNAL_KEY || '' const TIMEOUT_MS = 4000 async function call(path, { method = 'GET', body } = {}) { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const res = await fetch(`${BASE_URL}${path}`, { method, headers: { 'Content-Type': 'application/json', 'X-Internal-Key': KEY, }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }) if (!res.ok) { return { ok: false, error: `bot responded ${res.status}` } } return { ok: true, data: await res.json() } } catch (err) { log.warn('bot internal call failed', { path, message: err.message }) return { ok: false, error: err.message } } finally { clearTimeout(timeout) } } // Push a config change (start/stop the bot's Discord client). function pushConfig({ token, guildId, enabled }) { return call('/internal/config', { method: 'POST', body: { token, guildId, enabled } }) } // Live connection status, for the admin panel. function getStatus() { return call('/internal/status') } // Site -> bot: a news post was published, post it to the configured #news // channel. Fire-and-forget from the caller's perspective — never throws, so // a bot outage never breaks publishing a post. function announce({ title, excerpt, url, imageUrl }) { return call('/internal/announce', { method: 'POST', body: { title, excerpt, url, imageUrl } }) } module.exports = { pushConfig, getStatus, announce }