// The outbound DM path for the bus notifier (packages/cli, lead decision 70). // It uses the connector's bot token and REST client but not its gateway // process: one binding, one recipient (`dmRecipient`, which must be a listed // user), one DM channel opened on first use and kept in memory. // // The caller sees outcomes only: `{messageId}` on a confirmed send, or a // RestOutcome (refused | unknown) it journals and retries. The recipient id, // the channel id and the token never leave this module: not in a return // value, an error message or a log line. import { DiscordError } from "./errors.mjs"; import { bindingPath, loadBinding, readToken } from "./binding.mjs"; import { createRest, API_BASE, RestOutcome } from "./rest.mjs"; export function openDirect({ dataRoot, name, fetch = globalThis.fetch, base = API_BASE, sleep, log = () => {} }) { const binding = loadBinding(bindingPath(dataRoot, name)); if (binding.dmRecipient === null) throw new DiscordError(`binding ${name}: no dmRecipient; the notifier has nobody to DM`, 2); const rest = createRest({ token: readToken(binding), fetch, base, sleep, log }); const recipient = binding.dmRecipient; let channel = null; return Object.freeze({ binding: binding.name, // Resolves {messageId}. Throws RestOutcome (refused | unknown). The // nonce makes Discord return the earlier message when an unknown send // is retried within its dedupe window. async send({ content, nonce }) { if (channel === null) { try { channel = await rest.createDm(recipient); } catch (err) { if (err instanceof RestOutcome) throw new RestOutcome(err.kind, `dm channel: ${err.kind}`, { status: err.details.status }); throw err; } } try { const { messageId } = await rest.createMessage(channel, { content, nonce }); return { messageId }; } catch (err) { if (!(err instanceof RestOutcome)) throw err; // A refusal can mean the channel went away; open it again next time. if (err.kind === "refused") channel = null; throw new RestOutcome(err.kind, `dm: ${err.kind}`, { status: err.details.status }); } }, }); }