Skip to content

feat(sender): add chat-style UI for sending and receiving messages - #182

Open
prakash-dev-code wants to merge 1 commit into
evolution-foundation:mainfrom
prakash-dev-code:feat/sender-chat-ui
Open

feat(sender): add chat-style UI for sending and receiving messages#182
prakash-dev-code wants to merge 1 commit into
evolution-foundation:mainfrom
prakash-dev-code:feat/sender-chat-ui

Conversation

@prakash-dev-code

@prakash-dev-code prakash-dev-code commented Aug 26, 2026

Copy link
Copy Markdown

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.

  • Plain HTML/CSS/JS in web/sender/index.html. No build step, so the repo gains
    no frontend toolchain. Kept out of manager/dist because that bundle is
    generated upstream.
  • Registered from main.go via sender_handler.RegisterRoutes rather than
    pkg/routes, because the page needs *config.Config to bootstrap. Mirrors
    how the passkey ceremony routes are wired.
  • Routes: GET /sender (+ /sender/, /chat) and a GET / redirect, since
    / was previously a 404.
  • Uses only existing endpoints: /instance/all, /instance/connect,
    /send/text, /send/media, /message/downloadmedia, /ws. No changes to
    existing handlers or services.
  • New GET /sender/resolve-lids maps LID identifiers to phone numbers via the
    whatsmeow 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

  • The global API key is embedded in the page only for loopback requests.
    RemoteIP is used rather than ClientIP, which honours X-Forwarded-For
    and can be spoofed. SENDER_DISABLE_KEY_AUTOFILL turns it off entirely.
    Happy to make this opt-in instead if you'd prefer a stricter default.
  • Message bodies render via textContent, never innerHTML.

Type of Change

  • New feature (non-breaking change which adds functionality)

Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced

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 not
regenerated: swag init currently exits 1 on pkg/call/handler/call_handler.go
(pre-existing, unrelated to this change), so no output is produced. Note that
the swagger Makefile target prints a success message regardless because it
uses ; rather than && after the swag call — 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:

  • Add a standalone chat-style sender interface at /sender with saved contacts, conversation history, text and media messaging, live inbound updates, and delivery/read status tracking.
  • Add authenticated LID-to-phone-number resolution for labeling WhatsApp conversations with human-readable recipients.

Bug Fixes:

  • Redirect the root path to the sender interface instead of returning a 404.

Enhancements:

  • Register sender routes with access to application configuration and restrict automatic API-key injection to loopback requests, with an opt-out environment setting.

Build:

  • Include the standalone sender web assets in the production Docker image without adding a frontend build toolchain.

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.
@sourcery-ai

sourcery-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds 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 messages

sequenceDiagram
    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
Loading

Sequence diagram for secure sender page bootstrap and LID resolution

sequenceDiagram
    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
Loading

Flow diagram for sender route registration and packaging

flowchart 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]
Loading

File-Level Changes

Change Details Files
Adds a standalone chat-style sender application with persisted contacts, per-conversation history, text/media composition, inbound message handling, live status updates, and responsive desktop/mobile layouts.
  • Implements the complete no-build HTML/CSS/JavaScript UI.
  • Persists contacts, threads, settings, and media descriptors in localStorage while reloading media on demand.
  • Uses existing instance, send, media-download, and WebSocket APIs for messaging and delivery/read updates.
  • Handles WhatsApp LID-addressed chats and merges resolved LID conversations into phone-numbered threads.
  • Escapes message content through DOM text APIs rather than HTML rendering.
web/sender/index.html
Introduces server-side routing and page bootstrapping for the sender UI, including guarded LID resolution and loopback-only API-key injection.
  • Registers /, /sender, /sender/, and /chat directly from main.go with access to application configuration.
  • Serves the static page with no-store caching and injects JSON bootstrap configuration.
  • Adds an API-key-protected endpoint that parameterizes numeric LID lookups against the PostgreSQL whatsmeow mapping table.
  • Restricts automatic key embedding to socket-loopback requests and supports disabling it through SENDER_DISABLE_KEY_AUTOFILL.
  • Returns an empty mapping when LID storage is unavailable or lookups fail.
pkg/sender/handler/sender_handler.go
cmd/evolution-go/main.go
pkg/routes/routes.go
Packages the new static sender assets into the production container image.
  • Copies the web directory from the build stage into the runtime image.
Dockerfile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +141 to +142
c.JSON(http.StatusOK, out)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread web/sender/index.html
Comment on lines +1226 to +1234
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(); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant