|
@@ -14,8 +14,8 @@ sources:
|
|
|
resource: repo://src/whatsapp.rs
|
|
resource: repo://src/whatsapp.rs
|
|
|
verified:
|
|
verified:
|
|
|
- by: openwiki/0.7.2
|
|
- by: openwiki/0.7.2
|
|
|
- at: 2026-10-24T16:42:08.550Z
|
|
|
|
|
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
|
|
|
|
|
|
|
+ at: 2026-10-25T13:15:27.881Z
|
|
|
|
|
+generated: { by: "claude-code", at: "2026-10-25T13:15:27.881Z" }
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
# Message history store
|
|
# Message history store
|
|
@@ -26,9 +26,9 @@ own — the WhatsApp session lives in a separate database owned by
|
|
|
`whatsapp-rust` (see
|
|
`whatsapp-rust` (see
|
|
|
[Configuration and data](../operations/configuration-and-data.md)). `main`
|
|
[Configuration and data](../operations/configuration-and-data.md)). `main`
|
|
|
opens it before building the app and passes it in `AppDeps`
|
|
opens it before building the app and passes it in `AppDeps`
|
|
|
-(`repo://src/main.rs#L51-L68`, `repo://src/app.rs#L193-L199`);
|
|
|
|
|
|
|
+(`repo://src/main.rs#L61-L78`, `repo://src/app.rs#L212-L218`);
|
|
|
[`App`](../architecture/app-state.md) keeps it as a private field
|
|
[`App`](../architecture/app-state.md) keeps it as a private field
|
|
|
-(`repo://src/app.rs#L233`) and calls it synchronously from the event loop, so
|
|
|
|
|
|
|
+(`repo://src/app.rs#L252`) and calls it synchronously from the event loop, so
|
|
|
all writes happen on the loop thread. Failures come back as `Err` and end up
|
|
all writes happen on the loop thread. Failures come back as `Err` and end up
|
|
|
either as a startup abort or in the `Errore storico: …` status line.
|
|
either as a startup abort or in the `Errore storico: …` status line.
|
|
|
|
|
|
|
@@ -139,7 +139,7 @@ message; own messages never name a chat (`chat_name_candidate`,
|
|
|
The boolean return value drives the app: `on_message` returns immediately on
|
|
The boolean return value drives the app: `on_message` returns immediately on
|
|
|
`false`, so history-sync replays and redeliveries neither refresh the UI nor
|
|
`false`, so history-sync replays and redeliveries neither refresh the UI nor
|
|
|
trigger the [AI autoreply](../workflows/ai-autoreply.md)
|
|
trigger the [AI autoreply](../workflows/ai-autoreply.md)
|
|
|
-(`repo://src/app.rs#L485-L499`).
|
|
|
|
|
|
|
+(`repo://src/app.rs#L531-L545`).
|
|
|
|
|
|
|
|
## Reading
|
|
## Reading
|
|
|
|
|
|
|
@@ -153,7 +153,7 @@ trigger the [AI autoreply](../workflows/ai-autoreply.md)
|
|
|
recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to ascending
|
|
recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to ascending
|
|
|
order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to load the
|
|
order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to load the
|
|
|
context from local midnight of yesterday, at most `CONTEXT_MESSAGES` messages
|
|
context from local midnight of yesterday, at most `CONTEXT_MESSAGES` messages
|
|
|
- (`repo://src/app.rs#L991-L998`).
|
|
|
|
|
|
|
+ (`repo://src/app.rs#L1037-L1044`).
|
|
|
- `list_chats()` (`repo://src/history.rs#L175-L191`): every `chats` row ordered
|
|
- `list_chats()` (`repo://src/history.rs#L175-L191`): every `chats` row ordered
|
|
|
by `last_timestamp DESC, jid`; the name is the contact name, else the chat
|
|
by `last_timestamp DESC, jid`; the name is the contact name, else the chat
|
|
|
name, else the user part of the JID, resolved in `chat_summary_from_row`
|
|
name, else the user part of the JID, resolved in `chat_summary_from_row`
|
|
@@ -174,7 +174,7 @@ lookups (`display_name`, `is_autoreply`, `is_global_autoreply`,
|
|
|
`upsert_contact` (`repo://src/history.rs#L197-L211`) inserts or renames a
|
|
`upsert_contact` (`repo://src/history.rs#L197-L211`) inserts or renames a
|
|
|
contact and ignores blank names, so an empty push name never erases a known
|
|
contact and ignores blank names, so an empty push name never erases a known
|
|
|
name. `on_contact` stores the contact and reloads the lists
|
|
name. `on_contact` stores the contact and reloads the lists
|
|
|
-(`repo://src/app.rs#L465-L477`).
|
|
|
|
|
|
|
+(`repo://src/app.rs#L511-L523`).
|
|
|
|
|
|
|
|
## Autoreply flags
|
|
## Autoreply flags
|
|
|
|
|
|
|
@@ -187,13 +187,13 @@ name. `on_contact` stores the contact and reloads the lists
|
|
|
of `settings`; the default is `false` when it was never stored. The global
|
|
of `settings`; the default is `false` when it was never stored. The global
|
|
|
flag is independent of the per-chat flags: toggling it never changes a
|
|
flag is independent of the per-chat flags: toggling it never changes a
|
|
|
`chats.autoreply` value, and a chat is answered when either is on
|
|
`chats.autoreply` value, and a chat is answered when either is on
|
|
|
- (`repo://src/app.rs#L854-L860`).
|
|
|
|
|
|
|
+ (`repo://src/app.rs#L900-L906`).
|
|
|
|
|
|
|
|
Two consequences of `set_autoreply` upserting the row: a chat toggled on from
|
|
Two consequences of `set_autoreply` upserting the row: a chat toggled on from
|
|
|
the contacts list exists in `chats` with `last_timestamp = 0` and no messages,
|
|
the contacts list exists in `chats` with `last_timestamp = 0` and no messages,
|
|
|
so it is listed (sorted last, named by the JID user part until a contact or a
|
|
so it is listed (sorted last, named by the JID user part until a contact or a
|
|
|
message names it), and the startup catch-up skips it because it has no last
|
|
message names it), and the startup catch-up skips it because it has no last
|
|
|
-message to inspect (`repo://src/app.rs#L964-L981`). New messages never reset the
|
|
|
|
|
|
|
+message to inspect (`repo://src/app.rs#L1010-L1027`). New messages never reset the
|
|
|
per-chat flag, because the message upsert only touches `last_timestamp` and
|
|
per-chat flag, because the message upsert only touches `last_timestamp` and
|
|
|
`name`.
|
|
`name`.
|
|
|
|
|
|
|
@@ -217,7 +217,7 @@ preserved.
|
|
|
|
|
|
|
|
The only caller is the transcription step of the
|
|
The only caller is the transcription step of the
|
|
|
[AI autoreply](../workflows/ai-autoreply.md) workflow
|
|
[AI autoreply](../workflows/ai-autoreply.md) workflow
|
|
|
-(`repo://src/app.rs#L784-L801`). For each transcribed audio whose message still
|
|
|
|
|
|
|
+(`repo://src/app.rs#L830-L847`). For each transcribed audio whose message still
|
|
|
exists it stores
|
|
exists it stores
|
|
|
|
|
|
|
|
```
|
|
```
|
|
@@ -226,7 +226,7 @@ exists it stores
|
|
|
|
|
|
|
|
i.e. the constant `VOICE_NOTE_TEXT` (`repo://src/app.rs#L38-L41`, a deliberate
|
|
i.e. the constant `VOICE_NOTE_TEXT` (`repo://src/app.rs#L38-L41`, a deliberate
|
|
|
duplicate of the private `VOICE_PLACEHOLDER` in
|
|
duplicate of the private `VOICE_PLACEHOLDER` in
|
|
|
-`repo://src/whatsapp.rs#L45-L46`), a space, and the transcript. The previous
|
|
|
|
|
|
|
+`repo://src/whatsapp.rs#L46-L47`), a space, and the transcript. The previous
|
|
|
body was exactly `[messaggio vocale]`, so this is an **overwrite** with the
|
|
body was exactly `[messaggio vocale]`, so this is an **overwrite** with the
|
|
|
placeholder re-created as a prefix, not an append to arbitrary text. The write is
|
|
placeholder re-created as a prefix, not an append to arbitrary text. The write is
|
|
|
guarded by `message_exists`, and the `bool` returned by `update_message_text` is
|
|
guarded by `message_exists`, and the `bool` returned by `update_message_text` is
|
|
@@ -240,19 +240,19 @@ record that an audio was transcribed.
|
|
|
### Why the placeholder stops matching
|
|
### Why the placeholder stops matching
|
|
|
|
|
|
|
|
`is_voice_note` returns `true` only when the trimmed text equals
|
|
`is_voice_note` returns `true` only when the trimmed text equals
|
|
|
-`[messaggio vocale]` exactly (`repo://src/whatsapp.rs#L1174-L1181`). Once a
|
|
|
|
|
|
|
+`[messaggio vocale]` exactly (`repo://src/whatsapp.rs#L1228-L1235`). Once a
|
|
|
transcription is stored the body is `[messaggio vocale] <transcription>`, which
|
|
transcription is stored the body is `[messaggio vocale] <transcription>`, which
|
|
|
no longer matches, and two consumers depend on that:
|
|
no longer matches, and two consumers depend on that:
|
|
|
|
|
|
|
|
- `audios_to_transcribe` treats a message as already transcribed when an audio
|
|
- `audios_to_transcribe` treats a message as already transcribed when an audio
|
|
|
is in progress **or** its stored text is no longer the plain placeholder
|
|
is in progress **or** its stored text is no longer the plain placeholder
|
|
|
- (`repo://src/app.rs#L900-L918`), so each audio is transcribed once even though
|
|
|
|
|
|
|
+ (`repo://src/app.rs#L946-L964`), so each audio is transcribed once even though
|
|
|
nothing else records it — and an audio whose transcription failed keeps the
|
|
nothing else records it — and an audio whose transcription failed keeps the
|
|
|
bare placeholder.
|
|
bare placeholder.
|
|
|
- `user_content` in `src/autoreply.rs` replaces the body with the
|
|
- `user_content` in `src/autoreply.rs` replaces the body with the
|
|
|
"the contact sent a voice message you cannot listen to" sentinel only while it
|
|
"the contact sent a voice message you cannot listen to" sentinel only while it
|
|
|
still matches, otherwise it passes the stored text through
|
|
still matches, otherwise it passes the stored text through
|
|
|
- (`repo://src/autoreply.rs#L364-L379`). A transcribed voice note therefore
|
|
|
|
|
|
|
+ (`repo://src/autoreply.rs#L372-L387`). A transcribed voice note therefore
|
|
|
reaches the model as `[messaggio vocale] <transcription>` in that reply and in
|
|
reaches the model as `[messaggio vocale] <transcription>` in that reply and in
|
|
|
every later context built from the store.
|
|
every later context built from the store.
|
|
|
|
|
|
|
@@ -277,20 +277,20 @@ of downloading and describing the image again:
|
|
|
|
|
|
|
|
- `message_exists(message_id)` (`repo://src/history.rs#L323-L332`) tells whether
|
|
- `message_exists(message_id)` (`repo://src/history.rs#L323-L332`) tells whether
|
|
|
a message is still stored. The app checks it before saving a description
|
|
a message is still stored. The app checks it before saving a description
|
|
|
- (`repo://src/app.rs#L747-L759`), so a chat deleted while its images were being
|
|
|
|
|
|
|
+ (`repo://src/app.rs#L793-L805`), so a chat deleted while its images were being
|
|
|
described gets no orphan rows.
|
|
described gets no orphan rows.
|
|
|
- `save_image_description(message_id, chat_jid, description)`
|
|
- `save_image_description(message_id, chat_jid, description)`
|
|
|
(`repo://src/history.rs#L352-L363`) upserts the row, replacing a previous
|
|
(`repo://src/history.rs#L352-L363`) upserts the row, replacing a previous
|
|
|
description of the same message.
|
|
description of the same message.
|
|
|
- `image_descriptions(chat_jid)` (`repo://src/history.rs#L369-L381`) returns the
|
|
- `image_descriptions(chat_jid)` (`repo://src/history.rs#L369-L381`) returns the
|
|
|
chat's descriptions keyed by message id; the app uses it to skip images
|
|
chat's descriptions keyed by message id; the app uses it to skip images
|
|
|
- already described (`repo://src/app.rs#L882-L898`) and to turn image messages
|
|
|
|
|
|
|
+ already described (`repo://src/app.rs#L928-L944`) and to turn image messages
|
|
|
into text when building the AI context
|
|
into text when building the AI context
|
|
|
- (`repo://src/app.rs#L1009-L1028`).
|
|
|
|
|
|
|
+ (`repo://src/app.rs#L1056-L1077`).
|
|
|
|
|
|
|
|
Descriptions are separate from `messages.text`, which keeps only the image
|
|
Descriptions are separate from `messages.text`, which keeps only the image
|
|
|
placeholder plus optional caption rendered by `src/whatsapp.rs`
|
|
placeholder plus optional caption rendered by `src/whatsapp.rs`
|
|
|
-(`repo://src/whatsapp.rs#L1085-L1103`); editing a body would never touch them,
|
|
|
|
|
|
|
+(`repo://src/whatsapp.rs#L1139-L1157`); editing a body would never touch them,
|
|
|
and only `delete_chat` removes them.
|
|
and only `delete_chat` removes them.
|
|
|
|
|
|
|
|
## Deleting a chat
|
|
## Deleting a chat
|
|
@@ -305,7 +305,7 @@ later message recreates the chat row through the message upsert.
|
|
|
|
|
|
|
|
The app calls it after a double Delete press in the chat list; `confirm_delete`
|
|
The app calls it after a double Delete press in the chat list; `confirm_delete`
|
|
|
also drops the scheduled reply and the kept images/audios of that chat and
|
|
also drops the scheduled reply and the kept images/audios of that chat and
|
|
|
-closes it if open (`repo://src/app.rs#L1249-L1272`).
|
|
|
|
|
|
|
+closes it if open (`repo://src/app.rs#L1298-L1321`).
|
|
|
|
|
|
|
|
## Tests
|
|
## Tests
|
|
|
|
|
|