updates
ober
fd8d867a4db51d1f32e4281fe60a3a9818b75d53
--- a/Makefile +++ b/Makefile @@ -56,7 +56,9 @@ run-tui: binary tui-shim test: binary $(JEXEC) tests/test-send-result.ss $(JEXEC) tests/test-rpc-demux.ss + $(JEXEC) tests/test-logdb-jsqlite.ss $(JEXEC) tests/test-receive-normalization.ss + $(JEXEC) tests/test-history-replay.ss ./$(BIN) --help >/dev/null && echo "smoke ok" install: binary --- a/signal/tui/main.ss +++ b/signal/tui/main.ss @@ -5,6 +5,7 @@ (export run-tui-terminal run-tui-terminal-with-logdb open-tui-message-log + history-replay-snapshot notification->chat-event notification->line) @@ -72,29 +73,13 @@ (let ([theme-name (or (load-theme-name) "opencode-dark")]) (unless (set-theme-by-name! theme-name) (set-theme-by-name! "opencode-dark"))) - (let ([state (make-tui-state acct - version - receive-mode - (tb-width) - (tb-height) - "" - "Backend connected." - #f - 0 - (list - (make-system-conversation - (list - "TUI connected. Waiting for Signal receive notifications."))) - 0 - 'chat - "" - 0 - removed - (make-hashtable equal-hash equal?) - logdb - #f - #f - (make-hashtable equal-hash equal?))]) + (let ([state (make-initial-tui-state acct + version + receive-mode + (tb-width) + (tb-height) + logdb + removed)]) (append-system-message! state (if logdb @@ -107,6 +92,31 @@ (preload-history! state) (event-loop state actor))))) + (def (make-initial-tui-state acct version receive-mode width height logdb removed) + (make-tui-state acct + version + receive-mode + width + height + "" + "Backend connected." + #f + 0 + (list + (make-system-conversation + (list + "TUI connected. Waiting for Signal receive notifications."))) + 0 + 'chat + "" + 0 + removed + (make-hashtable equal-hash equal?) + logdb + #f + #f + (make-hashtable equal-hash equal?))) + ;; Open the encrypted message log. Passphrase comes from JERBOA_SIGNAL_DB_KEY ;; if set (enables headless use), otherwise we prompt. A blank passphrase or a ;; missing shim disables logging and the TUI runs normally. @@ -131,7 +141,7 @@ (def (preload-history! state) (let ([logdb (tui-state-logdb state)]) - (when logdb + (if logdb (let loop ([rows (logdb-recent logdb *history-preload-limit*)] [count 0]) (cond @@ -141,34 +151,120 @@ state (string-append "Restored " (number->string count) - " messages from the encrypted log.")))] + " messages from the encrypted log."))) + count] [else (loop (cdr rows) - (+ count (if (apply-history-row! state (car rows)) 1 0)))]))))) + (+ count (if (apply-history-row! state (car rows)) 1 0)))])) + 0))) + + ;; Non-interactive diagnostic/test helper: rebuild the same initial state used + ;; by the TUI and replay encrypted log rows into it without opening termbox or + ;; signal-cli. Returns (restored-count conversation-snapshots). + (def (history-replay-snapshot logdb) + (let* ([state (make-initial-tui-state "test" + "test" + "manual" + 80 + 24 + logdb + (make-hashtable equal-hash equal?))] + [count (preload-history! state)]) + (list count + (conversation-history-snapshots + (tui-state-conversations state))))) + + (def (conversation-history-snapshots conversations) + (let loop ([xs conversations] [acc '()]) + (cond + [(null? xs) (reverse acc)] + [(string=? (conversation-id (car xs)) "system") + (loop (cdr xs) acc)] + [else + (loop (cdr xs) + (cons (conversation-history-snapshot (car xs)) acc))]))) + + (def (conversation-history-snapshot conv) + (list (conversation-id conv) + (conversation-title conv) + (conversation-kind conv) + (conversation-target conv) + (let loop ([msgs (conversation-messages conv)] [acc '()]) + (if (null? msgs) + (reverse acc) + (loop (cdr msgs) + (cons (chat-message-history-snapshot (car msgs)) acc)))))) + + (def (chat-message-history-snapshot msg) + (list (chat-message-direction msg) + (chat-message-sender msg) + (chat-message-text msg) + (chat-message-timestamp msg) + (chat-message-kind msg) + (chat-message-status msg))) ;; row = (direction conversation sender timestamp kind body); see logdb-recent. (def (apply-history-row! state row) - (let* ([direction (list-ref row 0)] - [conv-id (canonical-conversation-id state (list-ref row 1))] - [sender (list-ref row 2)] - [ts (list-ref row 3)] - [body (list-ref row 5)] - [kind-target (parse-conversation-id conv-id)]) - (and kind-target - (not (conversation-removed? state conv-id)) - (let* ([out? (string=? direction "out")] - [conv (ensure-conversation! state conv-id "" - (car kind-target) - (cdr kind-target))] - [msg (make-chat-message (if out? 'out 'in) - (if out? "You" sender) - body - (and (number? ts) (> ts 0) ts) - 'data - #f)]) + (let ([ev (history-row->message-event state row)]) + (and ev + (let* ([id (list-ref ev 0)] + [title (list-ref ev 1)] + [kind (list-ref ev 2)] + [target (list-ref ev 3)] + [msg (list-ref ev 4)] + [conv (ensure-conversation! state id title kind target)]) (append-message-to-conversation! conv msg) #t)))) + (def (history-row->message-event state row) + (and (list? row) + (>= (length row) 6) + (let* ([direction (list-ref row 0)] + [conv-id (canonical-conversation-id state (list-ref row 1))] + [sender (list-ref row 2)] + [ts (list-ref row 3)] + [kind-text (list-ref row 4)] + [body (list-ref row 5)] + [kind-target (parse-conversation-id conv-id)]) + (and kind-target + (not (conversation-removed? state conv-id)) + (let* ([out? (string=? direction "out")] + [kind (car kind-target)] + [target (cdr kind-target)] + [title (history-row-title state conv-id kind target + sender out?)] + [msg (make-chat-message (if out? 'out 'in) + (if out? "You" sender) + body + (and (number? ts) (> ts 0) ts) + (history-kind-symbol kind-text) + #f)]) + (list conv-id title kind target msg)))))) + + (def (history-row-title state conv-id kind target sender out?) + (let ([existing (find-conversation (tui-state-conversations state) conv-id)]) + (if existing + "" + (cond + [(and (eq? kind 'direct) (not out?) (non-empty-string? sender)) + sender] + [(eq? kind 'group) + (string-append "Group " (short-id target))] + [(non-empty-string? target) target] + [else conv-id])))) + + (def (history-kind-symbol kind) + (cond + [(not (string? kind)) 'data] + [(string=? kind "edit") 'edit] + [(string=? kind "remote-delete") 'remote-delete] + [(string=? kind "reaction") 'reaction] + [(string=? kind "sticker") 'sticker] + [(string=? kind "contact") 'contact] + [(string=? kind "preview") 'preview] + [(string=? kind "attachment") 'attachment] + [else 'data])) + ;; "direct:<target>" / "group:<id>" -> (kind . target), #f for anything else. (def (parse-conversation-id id) (cond