Skip to content

fix(whatsapp): combine sender number and literal formatting fixes - #1039

Merged
bensabic merged 4 commits into
mainfrom
combined/whatsapp-fixes
Oct 19, 2026
Merged

bensabic merged 4 commits into
mainfrom
combined/whatsapp-fixes

Conversation

@roboeve

@roboeve roboeve Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

This combines two WhatsApp adapter fixes so they can be reviewed and merged together: #1007 from @armancharan, which sends replies from the number that received the thread, and #909 from @luksch42, which keeps literal text intact when converting Markdown to WhatsApp formatting. Each lands as its own commit with a Co-authored-by trailer, and two follow-up commits build on them. It supersedes #1007 and #909, which can be closed once this merges.

Fixes #988, fixes #908.

A bot connected to more than one WhatsApp number used to reply, react, mark messages read, show typing indicators, and upload media from the configured phoneNumberId, even when the conversation came in on a different number. Outbound calls now use the phone number ID encoded in the thread ID. Because that ID becomes part of the Graph API path, it has to be numeric: a thread ID that doesn't decode, or whose phone number ID contains anything other than digits, throws a ValidationError before any request is made. Each call decodes the thread once and passes it down to the send helpers. The protected helpers still accept the recipient string they took before, and uploadMedia and resolveMedia still default to the configured number, so existing subclasses keep working. openDM and the single-argument markAsRead(messageId) keep using the configured number, since neither has an inbound thread to read from.

On the formatting side, text such as (~80 % easy), C:\Users\Luis, or snake_case_name used to pick up visible Markdown escapes in WhatsApp. WhatsApp has no escape syntax, so the converter now sends text as written and keeps a backslash only where the text would otherwise turn into formatting: a marker that forms a pair on one line, such as \_word\_, a marker touching a formatting marker, and a quote, bullet, or numbered-list marker at the start of a line. This means Literal \*\*stars\*\* now comes out escaped rather than as Literal **stars**, which reverses one assertion from #909. Bold, italic, and strikethrough are rendered from their AST nodes, so markers inside inline and fenced code stay unchanged, intraword italics no longer produce HTML character references like n, and hard breaks are sent as plain newlines.

To support this, stringifyMarkdown in chat gains a handlers option for platform-specific serialization, which is why chat gets a minor changeset:

const md = stringifyMarkdown(ast, {
  handlers: {
    text: (node) => node.value,
  },
});

A few edge cases remain. A marker pair split across formatting, such as ~5 **x** 6~, isn't detected. Intraword *emphasis* like 2*3*4 comes out as 2_3_4. Autolinks are still wrapped in <...>, as they are on main.

pnpm validate passes on the branch. The two carried-over commits from roboeve[bot] don't have a DCO sign-off yet; the follow-up commits do. I haven't sent messages through a live WhatsApp number.

roboeve Bot and others added 2 commits October 13, 2026 15:35
…1007)

Carried over from #1007 by @armancharan.

Co-authored-by: Arman Charan <armancharan@armancharan.com>
Carried over from #909 by @luksch42. The WhatsApp adapter page was restructured on main after #909 was opened, so its new Message formatting section is placed under the new layout.

Co-authored-by: Lukas Scholten <46937006+luksch42@users.noreply.github.com>
@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chat Ready Ready Preview, v0 Oct 19, 2026 2:03am UTC

Outbound calls decode the thread ID once and require a numeric phone
number ID before building a Graph API path from it. A thread ID that
doesn't decode, or whose phone number ID contains anything other than
digits, throws a ValidationError before any request is made.

The decoded thread is passed to the send, media, and recipient helpers
so each call resolves its sender once. The protected helpers also
accept the recipient string they took before, and uploadMedia and
resolveMedia default to the configured phone number ID, so existing
subclasses keep working.

Tests cover reactions, typing indicators, templates, interactive and
media sends, and media uploads on a second phone number, plus thread
IDs with non-numeric phone number IDs.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
WhatsApp has no escape syntax, so outgoing text is sent as written
except where it would turn into WhatsApp formatting. A backslash is
kept before a marker that forms a pair on one line, such as \_word\_,
before a marker that touches a formatting marker, and before a quote,
bullet, or numbered-list marker at the start of a line.

Italic is rendered by its own handler, so intraword or space-padded
emphasis no longer produces HTML character references. Hard breaks are
sent as plain newlines. The bold, italic, strikethrough, text, and
break handlers are defined once at module level. The nested formatting
test counts handler calls instead of measuring elapsed time.

The handlers option on stringifyMarkdown is documented, and the chat
changeset is a minor release because the option is new public API.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
@bensabic
bensabic marked this pull request as ready for review October 19, 2026 02:21
@bensabic
bensabic requested a review from a team October 19, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — e80806fc Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant