Add a read API to the encrypted log: signal_log_recent + logdb-recent

ober

26e0187fc5688375eb87459577e2e302a8b1ce7a

diff --git a/signal/log_shim.c b/signal/log_shim.c
index b82a27a..30f44ef 100644
--- a/signal/log_shim.c
+++ b/signal/log_shim.c
@@ -130,6 +130,52 @@ long long signal_log_count(void *handle) {
     return n;
 }
 
+/* Begin reading the most recent displayable rows, newest first. Only data
+ * and edit rows with a body and a conversation qualify -- receipts, typing
+ * indicators and bare events are noise for history. Returns a statement
+ * handle (step/text/int/close below) or NULL.
+ * Columns: 0 direction, 1 conversation, 2 sender, 3 timestamp, 4 kind, 5 body. */
+void *signal_log_recent(void *handle, long long limit) {
+    if (!handle) return NULL;
+    sqlite3 *db = (sqlite3 *)handle;
+    static const char *SQL =
+        "SELECT direction, conversation, sender, timestamp, kind, body"
+        " FROM messages"
+        " WHERE kind IN ('data','edit')"
+        "   AND direction IN ('in','out')"
+        "   AND conversation IS NOT NULL AND conversation <> ''"
+        "   AND body IS NOT NULL AND body <> ''"
+        " ORDER BY id DESC LIMIT ?;";
+    sqlite3_stmt *st = NULL;
+    if (sqlite3_prepare_v2(db, SQL, -1, &st, NULL) != SQLITE_OK)
+        return NULL;
+    sqlite3_bind_int64(st, 1, (sqlite3_int64)(limit > 0 ? limit : 0));
+    return (void *)st;
+}
+
+/* 1 = a row is available, 0 = done (or error -- treated as done). */
+int signal_log_row_step(void *stmt) {
+    if (!stmt) return 0;
+    return sqlite3_step((sqlite3_stmt *)stmt) == SQLITE_ROW ? 1 : 0;
+}
+
+/* Text column of the current row; "" for NULL. Valid until the next step. */
+const char *signal_log_row_text(void *stmt, int col) {
+    if (!stmt) return "";
+    const unsigned char *s = sqlite3_column_text((sqlite3_stmt *)stmt, col);
+    return s ? (const char *)s : "";
+}
+
+long long signal_log_row_int(void *stmt, int col) {
+    if (!stmt) return 0;
+    return (long long)sqlite3_column_int64((sqlite3_stmt *)stmt, col);
+}
+
+int signal_log_row_close(void *stmt) {
+    if (!stmt) return 0;
+    return sqlite3_finalize((sqlite3_stmt *)stmt);
+}
+
 /* Read a passphrase from the controlling terminal with echo disabled.
  * Returns a pointer to a static buffer (copied by the FFI into a Scheme string). */
 const char *signal_log_getpass(const char *prompt) {
@@ -156,6 +202,16 @@ int main(void) {
     /* Reopen with correct key: both rows (incl. the original) still present. */
     void *db2 = signal_log_open(path, "correct horse battery staple");
     long long n2 = db2 ? signal_log_count(db2) : -1;
+
+    /* Readback: only the data row qualifies (remote-delete has no body). */
+    int recent = 0, body_ok = 0;
+    void *st = db2 ? signal_log_recent(db2, 10) : NULL;
+    while (st && signal_log_row_step(st)) {
+        recent++;
+        body_ok = strcmp(signal_log_row_text(st, 5), "hello world") == 0
+               && signal_log_row_int(st, 3) == 1717000000000LL;
+    }
+    if (st) signal_log_row_close(st);
     if (db2) signal_log_close(db2);
 
     /* Wrong key must fail. */
@@ -163,8 +219,8 @@ int main(void) {
     int wrong_rejected = (db3 == NULL);
     if (db3) signal_log_close(db3);
 
-    printf("rows=%lld reopened=%lld wrong_key_rejected=%d\n",
-           n, n2, wrong_rejected);
-    return (n == 2 && n2 == 2 && wrong_rejected) ? 0 : 2;
+    printf("rows=%lld reopened=%lld recent=%d body_ok=%d wrong_key_rejected=%d\n",
+           n, n2, recent, body_ok, wrong_rejected);
+    return (n == 2 && n2 == 2 && recent == 1 && body_ok && wrong_rejected) ? 0 : 2;
 }
 #endif
diff --git a/signal/logdb.ss b/signal/logdb.ss
index 70668bf..25cecff 100644
--- a/signal/logdb.ss
+++ b/signal/logdb.ss
@@ -7,7 +7,7 @@
 
 (library (signal logdb)
   (export logdb-available?
-          logdb-open logdb-close logdb-put logdb-count
+          logdb-open logdb-close logdb-put logdb-count logdb-recent
           logdb-prompt-passphrase)
 
   (import (except (chezscheme)
@@ -84,6 +84,40 @@
       ((foreign-procedure "signal_log_count" (uptr) integer-64) handle)
       -1))
 
+  ;; The most recent displayable rows (data/edit with a body), oldest first.
+  ;; Each row is (direction conversation sender timestamp kind body) -- all
+  ;; strings except timestamp (integer ms, 0 when unknown). '() when the shim
+  ;; is missing, the handle is #f, or anything goes wrong: history is
+  ;; best-effort and must never block the TUI from starting.
+  (def (logdb-recent handle limit)
+    (if (and handle
+             (entry? "signal_log_recent")
+             (entry? "signal_log_row_step")
+             (entry? "signal_log_row_text")
+             (entry? "signal_log_row_int")
+             (entry? "signal_log_row_close"))
+      (guard (e [#t '()])
+        (let ([open (foreign-procedure "signal_log_recent" (uptr integer-64) uptr)]
+              [step (foreign-procedure "signal_log_row_step" (uptr) int)]
+              [text (foreign-procedure "signal_log_row_text" (uptr int) string)]
+              [num (foreign-procedure "signal_log_row_int" (uptr int) integer-64)]
+              [close (foreign-procedure "signal_log_row_close" (uptr) int)])
+          (let ([st (open handle limit)])
+            (if (= st 0)
+              '()
+              (dynamic-wind
+                (lambda () (void))
+                (lambda ()
+                  ;; Rows arrive newest-first; consing reverses to oldest-first.
+                  (let loop ([acc '()])
+                    (if (= (step st) 1)
+                      (loop (cons (list (text st 0) (text st 1) (text st 2)
+                                        (num st 3) (text st 4) (text st 5))
+                                  acc))
+                      acc)))
+                (lambda () (close st)))))))
+      '()))
+
   ;; Read a passphrase from the controlling terminal with echo off.
   (def (logdb-prompt-passphrase prompt)
     (if (entry? "signal_log_getpass")