From e79e44af79bd6037559d031bb450a5333d01e8a5 Mon Sep 17 00:00:00 2001 From: underthestars-zhy Date: Mon, 8 Jun 2026 18:48:48 -0700 Subject: [PATCH] fix(photon): use spectrum-ts reply builder for threaded messages Replace raw `{ replyTo }` send options with the `spectrumReply` content builder from spectrum-ts, which is the correct API for threading replies. Adds `maybeReplyContent` helper with graceful fallback to normal send when the reply target cannot be resolved. --- plugins/platforms/photon/sidecar/index.mjs | 43 +++++++++++++++++----- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/plugins/platforms/photon/sidecar/index.mjs b/plugins/platforms/photon/sidecar/index.mjs index 7e06ac8e656..dfc2d6a193a 100644 --- a/plugins/platforms/photon/sidecar/index.mjs +++ b/plugins/platforms/photon/sidecar/index.mjs @@ -69,9 +69,15 @@ if (!projectId || !projectSecret || !sharedToken) { // Lazy-load spectrum-ts so a missing install fails with a clear message // instead of a cryptic module-resolution error during import. -let Spectrum, imessage, attachment, voice, spectrumText; +let Spectrum, imessage, attachment, voice, spectrumReply, spectrumText; try { - ({ Spectrum, attachment, voice, text: spectrumText } = await import("spectrum-ts")); + ({ + Spectrum, + attachment, + voice, + reply: spectrumReply, + text: spectrumText, + } = await import("spectrum-ts")); ({ imessage } = await import("spectrum-ts/providers/imessage")); } catch (e) { console.error( @@ -395,6 +401,28 @@ async function resolveSpace(spaceId) { throw new Error(`unable to resolve space id ${spaceId}`); } +async function maybeReplyContent(space, builder, replyTo) { + if (!replyTo) return builder; + if (typeof space.getMessage !== "function") { + console.error("photon-sidecar: reply requested but space.getMessage is unavailable"); + return builder; + } + try { + const target = await space.getMessage(replyTo); + if (!target) { + console.error(`photon-sidecar: reply target ${replyTo} not found; sending normally`); + return builder; + } + return spectrumReply(builder, target); + } catch (e) { + console.error( + "photon-sidecar: failed to resolve reply target; sending normally: " + + (e && e.stack ? e.stack : String(e)) + ); + return builder; + } +} + const server = http.createServer(async (req, res) => { if (req.headers["x-hermes-sidecar-token"] !== sharedToken) { return unauthorized(res); @@ -423,9 +451,8 @@ const server = http.createServer(async (req, res) => { return badRequest(res, "spaceId and text are required"); } const space = await resolveSpace(spaceId); - const result = replyTo - ? await space.send(spectrumText(text), { replyTo }) - : await space.send(spectrumText(text)); + const content = await maybeReplyContent(space, spectrumText(text), replyTo); + const result = await space.send(content); return ok(res, { messageId: result?.id || result?.messageId || null }); } if (req.url === "/send-attachment") { @@ -447,10 +474,8 @@ const server = http.createServer(async (req, res) => { ? voice(path, Object.keys(opts).length ? opts : undefined) : attachment(path, Object.keys(opts).length ? opts : undefined); - const sendOpts = replyTo ? { replyTo } : undefined; - const result = sendOpts - ? await space.send(builder, sendOpts) - : await space.send(builder); + const content = await maybeReplyContent(space, builder, replyTo); + const result = await space.send(content); // iMessage delivers the caption as a separate bubble; send it // after the media so the attachment renders first.