Skip to content

[4.41/W4] WhatsApp & Messenger: mark_as_read, native replies, code fences, guarded downloads #239

Description

@patrick-chinchill

Summary

Wire WhatsApp and Messenger into the new #200/#193 core APIs and move their downloads onto the #204 guarded downloader: mark_as_read(thread_id, message_id) (back-compat shim for WhatsApp's one-argument form; Messenger mark_seen), WhatsApp native contextual replies and code-fence normalization, and guarded downloads with a per-hop host and credential policy.

Upstream changes

  • 18d4a230 feat(chat): add mark as read support (#820) — chat@4.38.0 — WhatsApp markAsRead(threadIdOrMessageId, messageId?, message?) sends message_id: messageId ?? threadIdOrMessageId, throws AdapterError("WhatsApp mark as read failed") on falsy success; Messenger markAsRead(threadId, _messageId) POSTs sender_action: "mark_seen" to me/messages. Core half: [4.41/C6] Thread.reply, Thread.mark_as_read, post_ephemeral options #200.
  • 83ede7ea feat(chat): add message reply support (#819) — chat@4.38.0 — WhatsApp postMessage delegates to send(threadId, message, replyId?); reply(threadId, messageId, message) adds context: {message_id} to the first outgoing message only (first text chunk, first media item, or the interactive). Core half: [4.41/C6] Thread.reply, Thread.mark_as_read, post_ephemeral options #200.
  • e71bfead fix(slack): preserve first line of incoming code blocks (#843) — chat@4.39.0 — WhatsApp part: toAst runs normalizeCodeFences(markdown, {convertText: fromWhatsAppFormat}), so the line after an opening fence stays code and WhatsApp markup converts only outside fences. Helper: [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193.
  • 7c269653 fix(adapters): restrict attachment credentials to trusted hosts (#859) — chat@4.39.0 — WhatsApp part: isWhatsAppMediaUrl accepts the exact configured Graph origin, or https with no port on fbcdn.net/fbsbx.com (or subdomains); otherwise NetworkError("Refusing to send the access token to an untrusted media URL") before the token is sent.
  • b6fa24c6 fix(adapters): guard attachment downloads across slack, discord, telegram, and whatsapp (#865) — chat@4.39.0 — WhatsApp part: downloadMedia step 2 uses shared downloadAttachment with a per-hop header policy (token only to policy hosts, re-checked every redirect), the 25 MB cap and the timeout.
  • 153bd964 fix(messenger): guard attachment downloads (#856) — chat@4.39.0 — ported inline by [4.41/MS1] Messenger: guard attachment downloads #234; moved onto the shared helper here.

Current Python behavior

  • src/chat_sdk/adapters/whatsapp/adapter.py:1093-1102 mark_as_read(self, message_id) has no success check; grep -rn 'mark_as_read' tests finds no test.
  • Messenger has no mark_as_read (grep -n 'mark_seen\|mark_as_read' on messenger/adapter.py finds nothing); start_typing (messenger/adapter.py:626-636) shows the sender_action pattern.
  • WhatsApp has no reply; post_message (:751-783), _send_single_text_message (:785), _send_text_message (:823), _send_interactive_message (:839) take no reply id.
  • whatsapp/format_converter.py:82-89 to_ast runs parse_markdown(self._from_whatsapp_format(platform_text)): markup inside fences is rewritten and the first code line can be lost.
  • adapter.py:680-750 download_media has a Python-only suffix allowlist (facebook.com, fbcdn.net, fbsbx.com, whatsapp.net, whatsapp.com) with no exact-Graph-origin allowance and no port check; step 2 session.get(download_url, headers={"Authorization": ...}) follows redirects with aiohttp defaults, and the read is unbounded with no timeout.
  • Messenger _download_attachment: as left by [4.41/MS1] Messenger: guard attachment downloads #234.

Scope

Out of scope

Porting notes

  • Back-compat shim. await adapter.mark_as_read("wamid.X") must keep working (upstream test "preserves the adapter-level message id signature"). Use is not None, not or, so an explicit "" message id is not replaced.
  • success check. Upstream's !response.success is truthiness. Recommended: is True (fails closed on "true" strings), recorded in the sync doc; bool(...) is the documented alternative.
  • Reply context. if reply_id: matches upstream (empty string = no context). Clear remaining_id after the first send of any kind, including a leading text sent before media.
  • Credentials per hop. Compute Authorization per hop in the callback, never once on the session. The Graph allowance is an exact origin match (scheme, host, port), not a suffix match.
  • Rehydration. download_media is also reached via rehydrate_attachment (adapter.py:656-678); the rebuilt closure must use the same guarded path.

Tests

Upstream WhatsApp index.test.ts / markdown.test.ts and Messenger index.test.ts / fetch.test.ts are not fidelity-mapped. Port into tests/test_whatsapp_*.py / tests/test_messenger_*.py:

  • WhatsApp describe("markAsRead"): "marks an inbound message as read", "preserves the adapter-level message id signature", "rejects an unsuccessful API response".
  • Messenger describe("markAsRead"): "sends the mark_seen sender action".
  • WhatsApp describe("reply"): "adds contextual reply data to text messages", "adds contextual reply data only to the first split message", "adds contextual reply data to interactive cards", "adds reply context only to the first media message".
  • WhatsApp markdown.test.ts: "preserves code starting immediately after the opening fence", "does not rewrite bold or strikethrough inside fenced code", "keeps an unpaired ``` as literal text", "separates a fence from surrounding text on the same line".
  • WhatsApp describe("downloadMedia"): "rejects untrusted media URL %s", "downloads media from trusted Meta URL %s", "downloads media from the configured Graph origin", "refuses to send the token to off-policy redirect %s".
  • Messenger fetch.test.ts: the [4.41/MS1] Messenger: guard attachment downloads #234 tests keep passing unchanged (no duplicates).
  • Python-specific: a rehydrated WhatsApp attachment takes the guarded path. AsyncMock for Graph/Send calls, an injected fake transport for downloads, no sleeps.

Acceptance criteria

Dependencies

Blocked by #200, #193, #204. Soft ordering (not index blockers): lands after #234 (replaces its inline guard) and ideally after #238 (reply context on media); if #238 has not landed, "adds reply context only to the first media message" moves to #238.

Metadata

  • Effort: M (200-700 LOC)
  • Consumer impact: low. None for Slack/Teams; WhatsApp/Messenger get new APIs, stricter downloads, and mark_as_read raises on failure.
  • Suggested branch: sync/4.41-w4

Part of #184.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions