Skip to content

feat: persist inbound quoted_message_id and add reply support to /api/send#109

Open
blazer5000 wants to merge 1 commit into
verygoodplugins:mainfrom
blazer5000:feat/quoted-replies
Open

feat: persist inbound quoted_message_id and add reply support to /api/send#109
blazer5000 wants to merge 1 commit into
verygoodplugins:mainfrom
blazer5000:feat/quoted-replies

Conversation

@blazer5000
Copy link
Copy Markdown

Closes #107

What this adds

Inbound persistence — quoted-reply detection was already implemented in extractQuotedMessageInfo (used for webhook forwarding). This PR wires the extracted quotedMessageId into StoreMessage so replies are also archived in SQLite.

A new nullable quoted_message_id TEXT column is added via ensureColumn — the existing migration helper, so it's safe on all existing installs with no manual action needed.

Column Value
quoted_message_id StanzaID of the replied-to message (NULL for plain messages)

Plain messages store NULL; the COALESCE merge in ON CONFLICT means a later history-sync re-store (which passes "") cannot clobber a non-null ID already written by handleMessage.

Outbound quoted repliesPOST /api/send now accepts three optional fields:

{
  "recipient": "[email protected]",
  "message": "Great point!",
  "quoted_message_id": "3AORIGINAL0000001",
  "quoted_sender_jid": "[email protected]",
  "quoted_content": "original text"
}

When quoted_message_id is non-empty and no media_path is provided, the bridge sends an ExtendedTextMessage with a ContextInfo block so the message renders as a quoted reply on the recipient's device. Limitation: text-only — quoting media messages is not supported because the quote preview requires the original media key/URL, which the API caller does not have.

Outbound rows also persist quoted_message_id so list_messages can report it.

MCP serversend_message() and the send_message MCP tool both accept quoted_message_id, quoted_sender_jid, and quoted_content (all optional, default ""). They are forwarded to /api/send only when non-empty. list_messages and get_message_context now include quoted_message_id in every returned message dict (null for non-replies).

Tests

Go (whatsapp-bridge/main_test.go):

  • TestHandleMessage_QuotedReply_IDPersisted — inbound reply stores correct quoted_message_id
  • TestHandleMessage_PlainMessage_QuotedIDIsNull — plain message stores SQL NULL
  • TestSendHandler_QuotedReplyFields_PassedThrough — new JSON fields are parsed (empty recipient → 400)
  • TestExtractQuotedMessageInfo_ExtendedText — helper extracts StanzaID, Participant, and content
  • TestExtractQuotedMessageInfo_NoContextInfo — plain/nil message returns all-empty

Python (tests/test_bridge_auth.py, tests/test_whatsapp.py):

  • test_send_message_with_quoted_reply_includes_quote_fields — payload contains all three fields
  • test_send_message_without_quote_omits_quote_fields — no extra keys when not quoting
  • test_msg_to_dict_quoted_message_id_present/absent

All existing tests continue to pass (go test ./... and uv run pytest -v).

Schema impact

One new nullable column: messages.quoted_message_id TEXT. Added via ensureColumn — idempotent, backward-compatible, no downtime.

…/send

- Bridge: add quoted_message_id column to messages via ensureColumn
  (additive migration, safe on existing installs). StoreMessage gains a
  14th param; the value is stored as SQL NULL for plain messages.
  ON CONFLICT merge uses COALESCE so a history-sync re-store (passing "")
  never clobbers an ID already written by handleMessage.
  sendWhatsAppMessage gains quoted_message_id / quoted_sender_jid /
  quoted_content params. When quoted_message_id is set and no media
  path is provided, the bridge builds an ExtendedTextMessage with
  ContextInfo so the sent message renders as a quoted reply on the
  recipient's device. Outbound rows persist the quoted_message_id so
  list_messages can report it.
  SendMessageRequest adds three optional JSON fields:
  quoted_message_id, quoted_sender_jid, quoted_content.
- MCP server: send_message() and the send_message MCP tool both accept
  the three optional quote params and include them in the /api/send
  payload when present. list_messages and get_message_context SELECT
  quoted_message_id; msg_to_dict exposes it (null for non-replies).
- Tests: Go tests cover quoted_message_id persistence for inbound
  replies and plain messages, quote-field parsing at the /api/send
  boundary, and extractQuotedMessageInfo corner cases. Python tests
  cover quoted-field propagation in the payload and absence of fields
  when no quoted_message_id is given, plus msg_to_dict output.
- README: document quote params on send_message and inbound field.

Closes verygoodplugins#107
@blazer5000 blazer5000 requested a review from jack-arturo as a code owner May 23, 2026 07:18
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.

feat: persist quoted_message_id and send quoted replies

1 participant