feat(chatstorage): persist WhatsApp ContextInfo and expose in /chat/:jid/messages#648
feat(chatstorage): persist WhatsApp ContextInfo and expose in /chat/:jid/messages#648lucaslorentz wants to merge 1 commit intoaldinokemal:mainfrom
Conversation
WalkthroughAdds an optional Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Usecase as "Usecase\nGetChatMessages"
participant Repo as "SQLiteRepo\n(messages table)"
participant Domain as "Domain\nCreateMessage"
Client->>Usecase: Request messages
Usecase->>Repo: Query messages (includes context_metadata)
Repo-->>Usecase: Rows with context_metadata
Usecase-->>Client: Response (messages include context_metadata)
Note right of Repo: insertion path
Domain->>Repo: buildContextMetadata(contextInfo) -> store context_metadata
Repo-->>Domain: persist message with context_metadata
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.11.4)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/infrastructure/chatstorage/sqlite_repository.go (2)
1119-1135: Fix stale comment reference and consider extracting context metadata logic.Two issues on this block:
- The comment on line 1121 references
StoreMessageFromEvent, which does not exist in the repo. The inbound counterpart isCreateMessage. Update to avoid future confusion.- The local
ctx := map[string]any{}(line 1125) shadows the outerctx context.Contextparameter. The same pattern appears inCreateMessage(lines 856–871). Consider extracting both into a reusable helper function (e.g.,buildContextMetadata) to eliminate the shadowing and reduce duplication.🧹 Suggested refactoring
- // Capture ContextInfo (reply refs, ...) from the outgoing message too so - // bot-authored replies show up in /chat/:jid/messages with the same shape - // as inbound replies. Mirrors the logic in StoreMessageFromEvent. + // Capture ContextInfo (reply refs, ...) from the outgoing message too so + // bot-authored replies show up in /chat/:jid/messages with the same shape + // as inbound replies. Mirrors the logic in CreateMessage. var contextMetadata string if msg != nil { - if ci := utils.ExtractContextInfo(msg); ci != nil { - ctx := map[string]any{} - if stanzaID := ci.GetStanzaID(); stanzaID != "" { - ctx["replied_to_id"] = stanzaID - } - if len(ctx) > 0 { - if jsonBytes, err := json.Marshal(ctx); err == nil { - contextMetadata = string(jsonBytes) - } - } - } + contextMetadata = buildContextMetadata(utils.ExtractContextInfo(msg)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/infrastructure/chatstorage/sqlite_repository.go` around lines 1119 - 1135, Update the stale comment to reference the inbound counterpart CreateMessage instead of the non-existent StoreMessageFromEvent, and eliminate the local variable shadowing of the context.Context parameter by extracting the context-metadata construction into a reusable helper (e.g., buildContextMetadata) that returns the JSON string; replace the inline map creation in the current function and the CreateMessage function with calls to buildContextMetadata and use a different name for the context.Context parameter where needed to avoid shadowing (refer to utils.ExtractContextInfo, CreateMessage, and the current function when locating the code to change).
856-871: Rename the localctxmap — it shadows thecontext.Contextparameter, and extract a helper to dedupe withStoreSentMessageWithContext.Two concerns in this block:
- Shadowing
ctx context.Context.ctx := map[string]any{}on line 862 shadows the function'sctx context.Contextparameter. Although the shadow scope currently ends before any further context usage in this function, a future edit that adds actx-taking call inside thisifblock will silently misuse the map.go vet -vettool=shadowwould flag this.- Duplication. The exact same 10-line extraction pattern is repeated in
StoreSentMessageWithContext(lines 1125–1134). Extracting a small helper keeps future ContextInfo fields (mentions, forward info — per the doc comment) consistent across inbound and outbound paths.♻️ Proposed helper + call sites
Add a package-level helper (e.g., near the top of the file or next to
ExtractContextInfoinpkg/utils):// buildContextMetadata marshals whatsmeow ContextInfo-derived fields into a // JSON blob for storage. Returns "" when the message has no meaningful context. func buildContextMetadata(ci *waE2E.ContextInfo) string { if ci == nil { return "" } meta := map[string]any{} if stanzaID := ci.GetStanzaID(); stanzaID != "" { meta["replied_to_id"] = stanzaID } if len(meta) == 0 { return "" } jsonBytes, err := json.Marshal(meta) if err != nil { return "" } return string(jsonBytes) }Then in
CreateMessage:- // Capture whatsmeow ContextInfo (reply refs, ...) as a JSON blob so the - // info survives past the webhook event and shows up in /chat/:jid/messages - // responses. Structure is open so additional keys (mentions, forwards) - // can land without a new migration. - var contextMetadata string - if ci := utils.ExtractContextInfo(evt.Message); ci != nil { - ctx := map[string]any{} - if stanzaID := ci.GetStanzaID(); stanzaID != "" { - ctx["replied_to_id"] = stanzaID - } - if len(ctx) > 0 { - if jsonBytes, err := json.Marshal(ctx); err == nil { - contextMetadata = string(jsonBytes) - } - } - } + // Capture whatsmeow ContextInfo (reply refs, ...) as a JSON blob so the + // info survives past the webhook event and shows up in /chat/:jid/messages. + contextMetadata := buildContextMetadata(utils.ExtractContextInfo(evt.Message))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/infrastructure/chatstorage/sqlite_repository.go` around lines 856 - 871, The local map variable named ctx shadows the function parameter ctx context.Context and the same ContextInfo-to-JSON logic is duplicated in StoreSentMessageWithContext; fix by adding a helper function (e.g., buildContextMetadata(ci *waE2E.ContextInfo) string) that returns the JSON blob or "" and replace the inline map logic in both CreateMessage (the block using utils.ExtractContextInfo and the local ctx map) and StoreSentMessageWithContext with calls to buildContextMetadata; also rename/remove the local ctx map to avoid shadowing so the function parameter ctx remains unshadowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/infrastructure/chatstorage/sqlite_repository.go`:
- Around line 1119-1135: Update the stale comment to reference the inbound
counterpart CreateMessage instead of the non-existent StoreMessageFromEvent, and
eliminate the local variable shadowing of the context.Context parameter by
extracting the context-metadata construction into a reusable helper (e.g.,
buildContextMetadata) that returns the JSON string; replace the inline map
creation in the current function and the CreateMessage function with calls to
buildContextMetadata and use a different name for the context.Context parameter
where needed to avoid shadowing (refer to utils.ExtractContextInfo,
CreateMessage, and the current function when locating the code to change).
- Around line 856-871: The local map variable named ctx shadows the function
parameter ctx context.Context and the same ContextInfo-to-JSON logic is
duplicated in StoreSentMessageWithContext; fix by adding a helper function
(e.g., buildContextMetadata(ci *waE2E.ContextInfo) string) that returns the JSON
blob or "" and replace the inline map logic in both CreateMessage (the block
using utils.ExtractContextInfo and the local ctx map) and
StoreSentMessageWithContext with calls to buildContextMetadata; also
rename/remove the local ctx map to avoid shadowing so the function parameter ctx
remains unshadowed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d8aa6253-bf70-4833-b054-b31c59c7840d
📒 Files selected for processing (4)
src/domains/chat/chat.gosrc/domains/chatstorage/chatstorage.gosrc/infrastructure/chatstorage/sqlite_repository.gosrc/usecase/chat.go
…mment Address CodeRabbit nitpicks on aldinokemal#648: - Extract the inline ContextInfo → JSON marshalling into a private buildContextMetadata(*waE2E.ContextInfo) string helper so CreateMessage and StoreSentMessageWithContext share one path. Future ContextInfo fields (mentioned_jids, is_forwarded) only need to be added once. - Drop the local `ctx := map[string]any{}` that shadowed the outer context.Context parameter. - Fix a stale comment that referenced the non-existent StoreMessageFromEvent — the inbound counterpart is CreateMessage. No behavior change; output JSON shape is identical.
…jid/messages
Reply context (whatsmeow.ContextInfo.StanzaID) is available in real-time
webhook payloads (replied_to_id, quoted_body) but is not persisted.
Consumers that process messages asynchronously — notably anything
reacting to a prior message — have no way to know that message was a
reply, because GET /chat/:jid/messages doesn't expose that information.
Add a single context_metadata TEXT column (migration 17) storing a JSON
blob derived from ContextInfo. Shape is open-ended so fields like
mentioned_jids or is_forwarded can be added later without another
migration. Pattern matches call_metadata (migration 15) and
referral_metadata (migration 16).
Current scope captures only replied_to_id, which is enough to unblock
reply resolution in async consumers:
{"replied_to_id":"..."}
- infrastructure/chatstorage/sqlite_repository.go: migration 17,
INSERT/UPDATE/SELECT/scanMessage include the new column. A private
buildContextMetadata helper marshals ContextInfo into the JSON blob
and is shared by both CreateMessage (inbound) and
StoreSentMessageWithContext (outgoing) so bot-authored replies are
also recoverable.
- domains/chatstorage.Message: new ContextMetadata field.
- domains/chat.MessageInfo: new context_metadata field with omitempty
so responses for non-reply messages stay byte-identical.
- usecase/chat.go: mapper copies the field through.
Webhook payloads are untouched — replied_to_id and quoted_body still
appear as flat top-level fields (infrastructure/whatsapp/event_message.go).
Migration is additive; pre-existing messages get empty context_metadata,
which is omitted from responses.
Signed-off-by: Lucas Lorentz Lara <lucaslorentzlara@hotmail.com>
f4720f9 to
2cda0b0
Compare
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Reviewed by step-3.5-flash · 233,081 tokens |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/infrastructure/chatstorage/sqlite_repository.go (1)
30-50: LGTM with one optional refinement.The helper is correctly defensive: nil input, empty meta, and marshal failure all collapse to
"", which combines cleanly withomitemptyin the API model. Withmap[string]anyof plain strings,json.Marshalcannot realistically fail, so the silent fallback is fine. If this helper later grows to include richer types (mentions list, forward info, etc.), consider logging the marshal error so a future regression doesn't get swallowed.♻️ Optional: log the marshal failure path
jsonBytes, err := json.Marshal(meta) if err != nil { + logrus.WithError(err).Warn("failed to marshal context metadata") return "" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/infrastructure/chatstorage/sqlite_repository.go` around lines 30 - 50, The buildContextMetadata helper swallows json.Marshal errors silently; update buildContextMetadata (which accepts *waE2E.ContextInfo) to log the marshal failure before returning "" so future richer types don't lose diagnostics—capture the error from json.Marshal(meta) and call the package logger (or processLogger) with a clear message including the error and context (e.g., the meta contents or stanzaID) then return "" as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/infrastructure/chatstorage/sqlite_repository.go`:
- Around line 30-50: The buildContextMetadata helper swallows json.Marshal
errors silently; update buildContextMetadata (which accepts *waE2E.ContextInfo)
to log the marshal failure before returning "" so future richer types don't lose
diagnostics—capture the error from json.Marshal(meta) and call the package
logger (or processLogger) with a clear message including the error and context
(e.g., the meta contents or stanzaID) then return "" as before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e8f2dc38-7424-435d-b0ff-594da549abbb
📒 Files selected for processing (4)
src/domains/chat/chat.gosrc/domains/chatstorage/chatstorage.gosrc/infrastructure/chatstorage/sqlite_repository.gosrc/usecase/chat.go
🚧 Files skipped from review as they are similar to previous changes (2)
- src/domains/chatstorage/chatstorage.go
- src/domains/chat/chat.go
Problem
Reply context (
whatsmeow.ContextInfo.StanzaID) is available in real-time webhook payloads (replied_to_id,quoted_body) but is not persisted. Consumers that process messages asynchronously — notably anything reacting to a prior message — have no way to know that message was a reply, becauseGET /chat/:jid/messagesdoesn't expose that information.Concrete scenario: a bot listens to
message.reactionwebhooks, then callsGET /chat/:jid/messagesto resolve the reacted message. If the reacted message itself was a reply, the bot can't reconstruct that link — the field simply doesn't exist in the response.Approach
Add a single
context_metadata TEXT DEFAULT ''column to themessagestable (migration 17) storing a JSON blob derived fromContextInfo. Shape is open-ended so additional fields (mentions, forward info) can land later without another migration.Pattern matches
call_metadata(Migration 15) andreferral_metadata(Migration 16) — both JSON blobs stored alongside the core fields.Current scope
Only
replied_to_idis captured for now:```json
{"replied_to_id":"AC625C65EA7143E71D784176C4A90A93"}
```
Enough to unblock reply resolution.
quoted_body,mentioned_jids,is_forwarded, etc. can follow without schema changes.Changes
Response shape
For messages with context:
```json
{
"id": "AC4BFC5C3AD9CFB3797F8E9F8D2968A0",
"content": "got it",
"context_metadata": "{\"replied_to_id\":\"AC625C65EA7143E71D784176C4A90A93\"}",
...
}
```
Consumers `json.Unmarshal` when present. Messages without context omit the field entirely (`omitempty`), so existing responses look unchanged.
Webhook payloads unchanged
`replied_to_id` and `quoted_body` still show up as flat top-level fields in `message`-event webhook payloads (`infrastructure/whatsapp/event_message.go`). Existing webhook consumers are not affected.
Backward compatibility
Validation
Fresh build against `main` HEAD on darwin/arm64, deployed over a live instance with schema previously at version 14. Migrations 15–17 ran cleanly. Sent a WhatsApp reply; verified:
Related gap (separate)
`referral_metadata` is stored but not mapped into `MessageInfo` (no struct field, no mapper copy). Kept out of scope here; can follow up in a tiny separate PR if interested.