feat(sender): add chat-style UI for sending and receiving messages - #182
feat(sender): add chat-style UI for sending and receiving messages#182prakash-dev-code wants to merge 1 commit into
Conversation
The bundled manager (manager/dist) is a prebuilt artifact that manages instances but has no messaging screen, so there is currently no first-party way to send a message from a browser. Testing a paired instance means reaching for curl, Swagger or Postman, and re-entering the recipient number on every send. This adds a self-contained page at /sender: a chat list of saved recipients, per-conversation history, and a composer. Numbers are stored in the browser, so a recipient is reused with one click instead of being retyped. Implementation notes: - Plain HTML/CSS/JS in web/sender/index.html with no build step, so the repository gains no frontend toolchain. It is kept out of manager/dist because that bundle is generated upstream and would overwrite it. - Registered from main.go via sender_handler.RegisterRoutes rather than pkg/routes, because the page needs *config.Config to bootstrap itself. This mirrors how the passkey ceremony routes are wired. - Routes: GET /sender (plus /sender/ and /chat aliases) and a GET / redirect, since "/" was previously a 404 and the page is easy to miss. - Uses only existing endpoints: /instance/all, /instance/connect, /send/text, /send/media, /message/downloadmedia and the /ws socket. No changes to existing handlers or services. - Live updates arrive over the existing websocket producer. Inbound media renders from the base64 payload when MINIO_ENABLED is false; the media descriptor is kept so bytes can be re-fetched through /message/downloadmedia after a reload rather than persisting them. - Chats addressed by LID (for example 186896156205308@lid) are resolved to phone numbers via a new GET /sender/resolve-lids endpoint backed by the whatsmeow lid map, otherwise a conversation can only be labelled with an identifier that means nothing to a user. The endpoint requires the global API key because it discloses phone numbers, accepts numeric input only, and returns an empty object when the mapping is unavailable, such as on SQLite deployments. - The global API key is embedded in the page only for loopback requests, so a LAN or proxied client must enter it manually. RemoteIP is used rather than ClientIP, which honours X-Forwarded-For and can be spoofed. Setting SENDER_DISABLE_KEY_AUTOFILL turns the behaviour off entirely. Message bodies are rendered with textContent, never innerHTML, so inbound content cannot inject markup.
Reviewer's GuideAdds a self-contained /sender chat UI backed entirely by existing messaging and WebSocket APIs, with browser-persisted conversations, media support, LID resolution, new routing, guarded configuration bootstrapping, and Docker packaging. Sequence diagram for sending and receiving chat messagessequenceDiagram
participant User
participant UI as SenderUI
participant API as ExistingAPI
participant WS as WebSocket
participant WhatsApp
User->>UI: Compose message
UI->>API: sendText
API->>WhatsApp: Deliver message
WhatsApp-->>WS: Message and receipt events
WS-->>UI: Update conversation and status
WhatsApp-->>WS: Incoming message
WS-->>UI: Render received message
Sequence diagram for secure sender page bootstrap and LID resolutionsequenceDiagram
participant Browser
participant Sender as SenderHandler
participant DB as whatsmeow_lid_map
Browser->>Sender: GET /sender
alt Loopback and autofill enabled
Sender-->>Browser: HTML with bootstrap apiKey
else Remote or autofill disabled
Sender-->>Browser: HTML with empty bootstrap
end
Browser->>Sender: GET /sender/resolve-lids with apikey
Sender->>DB: Query numeric LIDs
DB-->>Sender: LID to phone mappings
Sender-->>Browser: JSON mapping
Flow diagram for sender route registration and packagingflowchart TD
Main[main.go setupRouter] --> Register[RegisterRoutes]
Register --> Routes[GET / and /sender routes]
Routes --> Page[Read web/sender/index.html]
Build[Docker build] --> Runtime[Runtime image]
Page --> Runtime
Runtime --> Browser[Serve sender UI]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="pkg/sender/handler/sender_handler.go" line_range="141-142" />
<code_context>
+ out[lid] = pn
+ }
+ }
+ c.JSON(http.StatusOK, out)
+}
+
+func itoa(n int) string {
</code_context>
<issue_to_address>
**issue (bug_risk):** resolveLIDs ignores rows.Err after iterating the PostgreSQL result. A connection or decoding failure after partial iteration is returned as HTTP 200 with an incomplete mapping, so the UI silently keeps displaying LIDs as if no mapping existed.
**Triggers:** When the database connection fails while rows.Next is consuming the result set.
**Suggested fix:** Check `rows.Err()` after the loop and return an error status instead of returning a partial successful mapping.
</issue_to_address>
### Comment 2
<location path="web/sender/index.html" line_range="1226-1234" />
<code_context>
+
+ if (ev === "Receipt") {
+ if (p.state !== "Read" && p.state !== "Delivered") return;
+ var ids = d.MessageIDs || [], rk = jidToKey(d.Chat);
+ if (lidMap[rk]) rk = lidMap[rk]; // receipts can arrive LID-addressed too
+ var list = threads[rk];
+ if (!list) return;
+ var hit = false;
+ list.forEach(function (m) {
+ if (m.dir === "out" && ids.indexOf(m.id) !== -1 && m.status !== "Read") { m.status = p.state; hit = true; }
+ });
+ if (hit) { saveThreads(); if (rk === current) renderMessages(); }
+ return;
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** Receipt handling derives the conversation key only from `d.Chat` and then returns when `threads[rk]` is absent. A receipt whose chat is represented by a LID while the message was stored under its resolved phone-number key is discarded unless `lidMap` already contains that mapping, so delivery/read ticks remain stuck at their previous state.
**Triggers:** When a receipt arrives for a LID-addressed chat before the LID mapping has been resolved or after the mapping is unavailable.
**Suggested fix:** Resolve the receipt's LID through the same pending/server mapping flow used for messages, or locate the outgoing message by its message ID across known threads before dropping the receipt.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and the UI embeds the global API key in loopback responses and stores it in browser localStorage, while also allowing the browser to send messages and change an instance's live-event configuration. If the behavior is wrong, messages may be sent to unintended recipients or the key may be exposed, and reverting cannot retract messages or undo any exposure that already occurred.
Blocking findings: pkg/sender/handler/sender_handler.go:142, web/sender/index.html:1234
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| c.JSON(http.StatusOK, out) | ||
| } |
There was a problem hiding this comment.
issue (bug_risk): resolveLIDs ignores rows.Err after iterating the PostgreSQL result. A connection or decoding failure after partial iteration is returned as HTTP 200 with an incomplete mapping, so the UI silently keeps displaying LIDs as if no mapping existed.
Triggers: When the database connection fails while rows.Next is consuming the result set.
Suggested fix: Check rows.Err() after the loop and return an error status instead of returning a partial successful mapping.
| var ids = d.MessageIDs || [], rk = jidToKey(d.Chat); | ||
| if (lidMap[rk]) rk = lidMap[rk]; // receipts can arrive LID-addressed too | ||
| var list = threads[rk]; | ||
| if (!list) return; | ||
| var hit = false; | ||
| list.forEach(function (m) { | ||
| if (m.dir === "out" && ids.indexOf(m.id) !== -1 && m.status !== "Read") { m.status = p.state; hit = true; } | ||
| }); | ||
| if (hit) { saveThreads(); if (rk === current) renderMessages(); } |
There was a problem hiding this comment.
issue (bug_risk): Receipt handling derives the conversation key only from d.Chat and then returns when threads[rk] is absent. A receipt whose chat is represented by a LID while the message was stored under its resolved phone-number key is discarded unless lidMap already contains that mapping, so delivery/read ticks remain stuck at their previous state.
Triggers: When a receipt arrives for a LID-addressed chat before the LID mapping has been resolved or after the mapping is unavailable.
Suggested fix: Resolve the receipt's LID through the same pending/server mapping flow used for messages, or locate the outgoing message by its message ID across known threads before dropping the receipt.
Description
Adds a self-contained chat UI at
/sender: a list of saved recipients,per-conversation history and a composer, so a number is reused with one click
instead of being retyped for each send.
web/sender/index.html. No build step, so the repo gainsno frontend toolchain. Kept out of
manager/distbecause that bundle isgenerated upstream.
main.goviasender_handler.RegisterRoutesrather thanpkg/routes, because the page needs*config.Configto bootstrap. Mirrorshow the passkey ceremony routes are wired.
GET /sender(+/sender/,/chat) and aGET /redirect, since/was previously a 404./instance/all,/instance/connect,/send/text,/send/media,/message/downloadmedia,/ws. No changes toexisting handlers or services.
GET /sender/resolve-lidsmaps LID identifiers to phone numbers via thewhatsmeow lid map, otherwise LID-addressed chats can only be labelled with a
meaningless identifier. Requires the global API key since it discloses phone
numbers, accepts numeric input only, returns
{}when unavailable (SQLite).Security notes
RemoteIPis used rather thanClientIP, which honoursX-Forwarded-Forand can be spoofed.
SENDER_DISABLE_KEY_AUTOFILLturns it off entirely.Happy to make this opt-in instead if you'd prefer a stricter default.
textContent, neverinnerHTML.Type of Change
Testing
Verified against a live paired instance: text send, media send, inbound
messages, delivery/read receipts, and LID resolution.
Additional Notes
Swagger annotations were added for the new endpoint, but
docs/was notregenerated:
swag initcurrently exits 1 onpkg/call/handler/call_handler.go(pre-existing, unrelated to this change), so no output is produced. Note that
the
swaggerMakefile target prints a success message regardless because ituses
;rather than&&after theswagcall — happy to file that separately.Summary by Sourcery
Add a self-contained chat interface for sending and receiving WhatsApp messages through Evolution GO.
New Features:
/senderwith saved contacts, conversation history, text and media messaging, live inbound updates, and delivery/read status tracking.Bug Fixes:
Enhancements:
Build: