Use signal-cli 0.14.5 for receive

ober

4e977e06aec781ddbb4e86268d8ce831bcc43fe5

diff --git a/Makefile b/Makefile
index 83fce58..4c3ac7b 100644
--- a/Makefile
+++ b/Makefile
@@ -62,6 +62,13 @@ test: binary
 install: binary
 	mkdir -p $(BIN_DIR)
 	install -m 0755 $(BIN) $(BIN_DIR)/$(BIN)
+	@if [ -f scripts/signal-cli-jvm ]; then \
+	  if [ -e "$(BIN_DIR)/signal-cli-jvm" ] && [ scripts/signal-cli-jvm -ef "$(BIN_DIR)/signal-cli-jvm" ]; then \
+	    :; \
+	  else \
+	    install -m 0755 scripts/signal-cli-jvm $(BIN_DIR)/signal-cli-jvm; \
+	  fi; \
+	fi
 	test ! -f signal_tui_shim.$(TUI_SHIM_EXT) || install -m 0755 signal_tui_shim.$(TUI_SHIM_EXT) $(BIN_DIR)/signal_tui_shim.$(TUI_SHIM_EXT)
 	@echo "Installed $(BIN) to $(BIN_DIR)/$(BIN)"
 
diff --git a/README.md b/README.md
index d1e5ab3..4b8c16c 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,10 @@ jerboa-signal tui [-a +PHONE]                      # terminal UI shell
 
 ## Prerequisites
 
-- `signal-cli` on `PATH` (Homebrew: `brew install signal-cli`)
+- `signal-cli` 0.14.5 or newer on `PATH` (Homebrew may lag; the local
+  `scripts/signal-cli-jvm` wrapper uses `~/.local/opt/signal-cli-0.14.5`).
+  Older 0.14.4.1 builds drop current sealed-sender receive envelopes with
+  `getServerGuid(...) must not be null`.
 - Jerboa build tools. `make binary`/`make install` use project-local
   `./jerbuild` first, then `.jerboa/bin`, then `../jerboa/dist`, then `PATH`,
   and download the matching release artifact if none are available.
diff --git a/scripts/signal-cli-jvm b/scripts/signal-cli-jvm
index 555466e..3b73777 100755
--- a/scripts/signal-cli-jvm
+++ b/scripts/signal-cli-jvm
@@ -1,3 +1,9 @@
 #!/bin/sh
 export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk}"
-exec "$HOME/.local/opt/signal-cli-0.14.4.1/bin/signal-cli" "$@"
+version="${JERBOA_SIGNAL_CLI_VERSION:-0.14.5}"
+bin="$HOME/.local/opt/signal-cli-$version/bin/signal-cli"
+if [ ! -x "$bin" ]; then
+  echo "signal-cli JVM tarball not found: $bin" >&2
+  exit 127
+fi
+exec "$bin" "$@"
diff --git a/signal/tui/main.ss b/signal/tui/main.ss
index a9e03ae..90fba6b 100644
--- a/signal/tui/main.ss
+++ b/signal/tui/main.ss
@@ -5,7 +5,8 @@
   (export run-tui-terminal
           run-tui-terminal-with-logdb
           open-tui-message-log
-          notification->chat-event)
+          notification->chat-event
+          notification->line)
 
   (import (except (chezscheme)
                   make-hash-table hash-table?
@@ -2425,9 +2426,12 @@
                 [account (or (hashtable-ref payload "account" #f)
                              (hashtable-ref params "account" #f)
                              "")]
+                [exception (and (hashtable? payload)
+                                (hashtable-ref payload "exception" #f))]
                 [envelope (and (hashtable? payload)
                                (hashtable-ref payload "envelope" #f))])
-           (and (hashtable? envelope)
+           (and (not (hashtable? exception))
+                (hashtable? envelope)
                 (envelope->chat-event account envelope)))))
 
   (def (envelope->chat-event account envelope)
@@ -2721,10 +2725,34 @@
               [account (or (hashtable-ref payload "account" #f)
                            (hashtable-ref params "account" #f)
                            "")]
+              [exception (and (hashtable? payload)
+                              (hashtable-ref payload "exception" #f))]
               [envelope (hashtable-ref payload "envelope" #f)])
-         (if (hashtable? envelope)
-           (envelope->line account envelope)
-           (string-append "Receive notification: " (safe-display payload))))]))
+         (cond
+           [(hashtable? exception)
+            (receive-exception->line account exception envelope)]
+           [(hashtable? envelope)
+            (envelope->line account envelope)]
+           [else
+            (string-append "Receive notification: " (safe-display payload))]))]))
+
+  (def (receive-exception->line account exception envelope)
+    (let* ([prefix (if (and (string? account) (not (string=? account "")))
+                    (string-append "[" account "] ")
+                    "")]
+           [source (if (hashtable? envelope)
+                     (or (hashtable-ref envelope "sourceName" #f)
+                         (hashtable-ref envelope "sourceNumber" #f)
+                         (hashtable-ref envelope "source" #f)
+                         "unknown")
+                     "unknown")]
+           [typ (first-non-empty-string
+                  (hashtable-ref exception "type" #f)
+                  "receive error")]
+           [msg (first-non-empty-string
+                  (hashtable-ref exception "message" #f)
+                  "signal-cli could not decode this receive envelope")])
+      (string-append prefix source ": receive failed: " typ ": " msg)))
 
   (def (envelope->line account envelope)
     (let* ([source (or (hashtable-ref envelope "sourceName" #f)
diff --git a/tests/test-receive-normalization.ss b/tests/test-receive-normalization.ss
index aca7641..517ae6e 100644
--- a/tests/test-receive-normalization.ss
+++ b/tests/test-receive-normalization.ss
@@ -108,6 +108,19 @@
                               "timestamp" 5000
                               "message" "from phone")))))
 
+(def receive-exception
+  (ht "jsonrpc" "2.0"
+      "method" "receive"
+      "params" (ht "subscription" 0
+                   "result" (ht "account" "+15550000"
+                                "exception" (ht "type" "NullPointerException"
+                                                "message"
+                                                "getServerGuid(...) must not be null")
+                                "envelope"
+                                (ht "sourceNumber" "+15550000"
+                                    "sourceName" "Ober"
+                                    "timestamp" 6000)))))
+
 (let ([event (notification->chat-event direct-uuid)])
   (check "direct sourceUuid promoted to chat event"
          (and (list? event) (eq? (car event) 'message)))
@@ -159,6 +172,13 @@
          (string=? (list-ref event 1)
                    "direct:33333333-4444-5555-6666-777777777777")))
 
+(check "receive exception is not converted to fake chat message"
+       (not (notification->chat-event receive-exception)))
+
+(check "receive exception line includes signal-cli error"
+       (string=? (notification->line receive-exception)
+                 "[+15550000] Ober: receive failed: NullPointerException: getServerGuid(...) must not be null"))
+
 (delete-if-exists! path)
 (delete-if-exists! (string-append path ".tmp"))