From dd9f0eeb66cb9cae76b9637d3c54f999f31fc3d8 Mon Sep 17 00:00:00 2001 From: Austin519 <548016+Austin519@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:40:25 -0600 Subject: [PATCH] Fall back to AppleScript when a Private API attachment send fails On macOS Tahoe (26.x) the BlueBubbles Private API helper throws NSInvalidArgumentException when sending an attachment, so photos/files sent from Matrix are silently dropped while text still works (text uses a different endpoint). The helper returns a non-2xx body rather than a transport error, so apiPostAsFormData reports success with an empty GUID and the drop goes unnoticed. SendFile now detects a failed Private API attachment send (non-2xx status or empty GUID) and retries via AppleScript. The AppleScript path can't target the "any" pseudo-service that portal GUIDs use ("Can't make any into type constant"), so resolve it to a concrete service first (ResolveIdentifier for DMs, iMessage for groups / on lookup failure). Non-private-api sends still deliver the caption as a follow-up message. --- imessage/bluebubbles/api.go | 74 ++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/imessage/bluebubbles/api.go b/imessage/bluebubbles/api.go index 3d3d86fa..b6c79635 100644 --- a/imessage/bluebubbles/api.go +++ b/imessage/bluebubbles/api.go @@ -1221,37 +1221,69 @@ func (bb *blueBubbles) SendFile(chatID, text, filename string, pathOnDisk string bb.log.Info().Int("attachmentSize", len(attachment)).Msg("Read attachment from disk") - var method string - if bb.usingPrivateAPI { - method = "private-api" - } else { - // we have to use apple-script and send a second message - method = "apple-script" + // Resolve the "any" pseudo-service to a concrete service for the AppleScript + // path (AppleScript errors "Can't make any into type constant" on "any"). + // For DMs, ask BlueBubbles which service the handle uses (iMessage vs SMS); + // for groups (and on lookup failure) default to iMessage. + appleScriptChatID := chatID + if id := imessage.ParseIdentifier(chatID); id.Service == "any" { + if !id.IsGroup { + if resolved, rerr := bb.ResolveIdentifier(id.LocalID); rerr == nil && resolved != "" { + appleScriptChatID = resolved + } else { + id.Service = "iMessage" + appleScriptChatID = id.String() + } + } else { + id.Service = "iMessage" + appleScriptChatID = id.String() + } } - formData := map[string]interface{}{ - "chatGuid": chatID, - "tempGuid": fmt.Sprintf("temp-%s", RandString(8)), - "name": filename, - "method": method, - "attachment": attachment, - "isAudioMessage": voiceMemo, - "selectedMessageGuid": replyTo, - "partIndex": replyToPart, + sendAttachment := func(method string) (SendTextResponse, error) { + chatGuid := chatID + if method == "apple-script" { + chatGuid = appleScriptChatID + } + formData := map[string]interface{}{ + "chatGuid": chatGuid, + "tempGuid": fmt.Sprintf("temp-%s", RandString(8)), + "name": filename, + "method": method, + "attachment": attachment, + "isAudioMessage": voiceMemo, + "selectedMessageGuid": replyTo, + "partIndex": replyToPart, + } + if method == "private-api" { + formData["subject"] = text + } + var resp SendTextResponse + reqErr := bb.apiPostAsFormData("/api/v1/message/attachment", formData, &resp) + return resp, reqErr } + method := "apple-script" if bb.usingPrivateAPI { - formData["subject"] = text + method = "private-api" } - path := "/api/v1/message/attachment" - - var response SendTextResponse - if err := bb.apiPostAsFormData(path, formData, &response); err != nil { + response, err := sendAttachment(method) + if err == nil && method == "private-api" && (response.Status >= 300 || response.Data.GUID == "") { + // The Private API attachment path can fail at the helper level (e.g. + // NSInvalidArgumentException on macOS Tahoe) while returning a non-2xx body + // rather than a transport error. Fall back to AppleScript. + bb.log.Warn().Int64("status", response.Status).Msg("Private API attachment send failed, falling back to AppleScript") + method = "apple-script" + response, err = sendAttachment(method) + } + if err != nil { return nil, err } - if !bb.usingPrivateAPI { + // The AppleScript attachment send carries no caption; deliver any caption as a + // separate follow-up message (guard against an empty caption). + if method != "private-api" && text != "" { bb.SendMessage(chatID, text, replyTo, replyToPart, nil, nil) }