Files
rmyndharis--openwa/docs/engine-capability-matrix.md
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

16 KiB
Raw Permalink Blame History

Engine Capability Matrix

This page is the operator- and engineer-facing view of which IWhatsAppEngine capabilities are real on each adapter — wwjs (whatsapp-web.js, the default) and baileys (the browser-free alternative) — and, for the ones that are missing, why and where to start.

The committed source of truth is src/engine/engine-capability-matrix.ts. Every IWhatsAppEngine method has a row with two adapters; each adapter is either supported (works end-to-end) or not-available, and each not-available cell carries a rootCause:

  • adapter-gap — the underlying library already HAS the capability; only the OpenWA adapter wiring is missing. Fixable in this repo with a PR that calls the library symbol the evidence points at.
  • library-limitation — the underlying library exposes no first-class symbol for the operation. Not fixable without a raw-proto/fork effort or an event-cache hack.
  • uncertain — the source trace was inconclusive; needs a live spike.

Each row also cites an evidence string (the exact library file:symbol inspected), so an engineer can open the file and start wiring immediately.

How the matrix stays honest

src/engine/engine-parity.spec.ts is a drift gate: it reads every adapter method body via Class.prototype.method.toString(), classifies it as supported/not-available by whether it throws EngineNotSupportedError / ChannelMediaNotSupportedError, and fails the build if the committed matrix drifts. No engine is instantiated and no Chromium/socket is opened — it is a fast, hermetic structural check. A parity change cannot land without deliberately updating this matrix and this doc.

The rootCause/evidence fields are hand-curated from source traces of the installed libraries (node_modules/@whiskeysockets/baileys, node_modules/whatsapp-web.js); they are not regenerated by the spec and are updated by hand as adapters are wired or libraries change.

Phantom support. The drift gate's throw-heuristic cannot see adapter methods that silently stub (return null/[] + a warn log) without throwing. Three wwjs entries — getCatalog, getProducts, getProduct — are marked not-available in the matrix even though their adapter bodies do not throw, because the library has no API for them. When the drift gate is extended to assert against this matrix, it must consult status, not just the throw pattern, for these rows (or the adapter stubs must start throwing).


Unwired-capability inventory

24 of the 71 interface methods are not-available on at least one adapter (31 not-available adapter-cells total). Grouped by cluster below. Each entry shows: status today → rootCause → evidence → wiring note.

Channels / Newsletter

Method baileys wwjs
getChannelMessages not-available — adapter-gap supported
getSubscribedChannels not-available — library-limitation supported

Wired. getChannelById, subscribeToChannel, unsubscribeFromChannel on Baileys — via newsletterMetadata('jid'|'invite', …)NewsletterMetadata mapped to Channel (id/name/description/inviteCode/subscriberCount/picture/verified/createdAt), newsletterFollow (subscribe, invite→jid bridge), newsletterUnfollow (unsubscribe, 1:1). getChannelById on Baileys resolves ANY channel by jid (richer than the wwjs subscribed-list lookup).

  • getChannelMessages (baileys, adapter-gap). sock.newsletterFetchMessages(channelId, limit, 0, 0) (Socket/newsletter.d.ts:19) returns the raw BinaryNode of <message_updates> children (newsletter.js:149). The fetch is one line; the real work is walking the children and mapping each to ChannelMessage{id,body,timestamp,hasMedia} — no library parser is exposed. Not wired: a hand-written BinaryNode walk can't be verified without a live WhatsApp session, so it stays a documented gap rather than an unverified implementation.
  • getSubscribedChannels (baileys, library-limitation). No enumerate-subscribed-newsletters query in the library. All 23 Socket/newsletter.d.ts exports are per-jid (newsletterMetadata requires a key; newsletterSubscribers(jid) returns the count of one newsletter). The newsletter event surfaces jids opportunistically during live sync, but that is incremental, not a list-all. Would require a raw WMex/app-state hack against an undocumented XWAPath.

Labels (WhatsApp Business)

Method baileys wwjs
getLabels not-available — library-limitation supported
getLabelById not-available — library-limitation supported
getChatLabels not-available — library-limitation supported

Wired. addLabelToChat / removeLabelFromChat on the Baileys engine — 1:1 to sock.addChatLabel(chatId, labelId) / sock.removeChatLabel(chatId, labelId) (Socket/chats.d.ts:70-71). WhatsApp-Business-only (rejects on personal accounts). Do not use addLabel(jid, LabelActionBody) (chats.d.ts:69) — that creates/edits the label definition, not the chat association.

  • getLabels / getLabelById / getChatLabels (baileys, library-limitation). No label read/fetch symbol anywhere in lib/**/*.d.ts (Types/Label.d.ts has only the interface + LabelColor enum; chats.d.ts/business.d.ts expose only writes). Label data does arrive via app-state sync (messaging-history.set), so a determined adapter could capture+cache labels from the event stream — but that is a relay/cache hack, not a first-class getter, and there is no network fetch to seed/refresh it on demand.

Catalog / Products / Orders (WhatsApp Business)

Method baileys wwjs
getCatalog not-available — adapter-gap (medium-confidence) not-available — library-limitation
getProducts not-available — adapter-gap not-available — library-limitation
getProduct not-available — adapter-gap (medium-confidence) not-available — library-limitation
sendProduct not-available — adapter-gap not-available — library-limitation
sendCatalog not-available — library-limitation not-available — library-limitation
  • getProducts (baileys, adapter-gap). sock.getCatalog({jid, limit, cursor}) (Socket/business.d.ts:7) returns {products, nextPageCursor} — paginated, maps to PaginatedProducts. Caveat: cursor-based, so total is unknown (approximate or iterate).
  • getCatalog (baileys, adapter-gap, medium-confidence). getCatalog returns a product list + cursor, not the OpenWA Catalog metadata wrapper. Name needs getCollections(jid) (business.d.ts:11); id/description/url have no source — the adapter would synthesize a partial Catalog (productCount=products.length, rest best-effort).
  • getProduct (baileys, adapter-gap, medium-confidence). No direct getProduct(id); call getCatalog({jid,limit}) then products.find(p=>p.id===productId) — loads the whole page to fetch one product.
  • sendProduct (baileys, adapter-gap). AnyRegularMessageContent accepts {product: WASendableProduct, businessOwnerJid, body} (Types/Message.d.ts:203, built in messages.js:397). Two-step wiring: getCatalog lookup to resolve the product's image/title/price, then sock.sendMessage(chatId, {product:{...}, body}). productId-only send without the lookup is not possible.
  • sendCatalog (both, library-limitation). No catalog-share message type exists in either library. Baileys AnyMessageContent has only {product} (single product); the catalog CRUD nodes (Socket/business.js:294-362) mutate the catalog, they don't send it. Would require raw-proto relay hacks (unverified).
  • wwjs catalog (library-limitation). whatsapp-web.js has no Client.getCatalog/getProducts/getProduct/sendProduct/sendCatalog (index.d.ts grep = 0 hits). Product/Order are inbound-only parsers. The adapter currently stubs the reads to null/[] without throwing (phantom support).

Status — post / delete

Wired. postTextStatus / postImageStatus / postVideoStatus + deleteStatus on whatsapp-web.js. Posts route via sendMessage('status@broadcast', …) ({ extra: { backgroundColor, fontStyle } } for text; { caption } for media); deleteStatus calls revokeStatusMessage(statusId) (own-status only). Caveat: whatsapp-web.js has no status-recipient arg, so StatusPostOptions.recipients is not honored on this engine (it broadcasts to the account's status-privacy audience; a one-time warning is logged). The Baileys engine honors recipients (statusJidList).

  • deleteStatus (baileys) caveat. Marked supported (no throw), but the adapter self-describes its sendMessage(status@broadcast,{delete}) revoke shape as empirically unverified (baileys.adapter.ts:909-911) — only posting was live-spiked. May need a fallback to EngineNotSupportedError if WA rejects the shape.

Status — read (contact stories)

Method baileys wwjs
getContactStatus not-available — library-limitation supported
getContactStatuses not-available — library-limitation supported

Wired. getContactStatus / getContactStatuses on whatsapp-web.js — getBroadcastById(id) / getBroadcasts() flattened to Status[] (contact via broadcast.getContact(); type from MessageTypes; 24h TTL). Caveat: Status.type is the text|image|video union — audio/other story types collapse to text.

  • getContactStatus / getContactStatuses (baileys, library-limitation). fetchStatus (Socket/chats.d.ts:42 via USyncStatusProtocol) returns the about/profile text line ({status, setAt}), not 24h stories. No story-read getter exists; story broadcasts surface only as status@broadcast messages via messages.upsert / messaging-history.set events. Would require OpenWA to accumulate status@broadcast messages itself and project them into Status[].

Messaging misc — delete / history / reactions

Method baileys wwjs
getChatHistory not-available — library-limitation supported
getMessageReactions not-available — library-limitation supported
  • getChatHistory (baileys, library-limitation). The only history primitive is fetchMessageHistory(count, oldestMsgKey, oldestMsgTimestamp) (Socket/business.d.ts:25) — it returns a sync-token string, not messages; the messages are delivered later via the messaging-history.set event. There is no per-chat fetchMessages(chatId, limit) on the socket. A synchronous Promise<IncomingMessage[]> for one chat would require an OpenWA-side chat-indexed store populated from messages.upsert + messaging-history.set events.
  • getMessageReactions (baileys, library-limitation). No on-demand server fetch. Reactions exist only as event-augmented state on WAMessage.reactions (proto.IReaction[] at WAProto/index.d.ts:10623), mutated by updateMessageWithReaction and surfaced via the messages.reaction event. The adapter already processes reactionMessage events (baileys.adapter.ts:1048-1057) and emits onMessageReaction, but it does not persist .reactions into its messageStore (early-returns at line 1058). A store-backed read would need that persistence added first; even then, only reactions observed since session start are known (no historical backfill).

Prioritized roadmap — adapter gaps (fixable in this repo)

These are the capabilities the underlying library already supports but the OpenWA adapter does not wire. Ranked high-value + low-effort first. Each is a self-contained backlog item; an engineer can open the cited symbol and start.

Progress. deleteMessage (forEveryone=false, Baileys) — wired via chatModify({ deleteForMe }); moved to supported.

Tier 1 — small effort, high value shipped

All Tier-1 adapter-gaps have been wired:

  • deleteMessage(forEveryone=false) — Baileys (chatModify({ deleteForMe }))
  • postTextStatus / postImageStatus / postVideoStatus — whatsapp-web.js (sendMessage('status@broadcast', …); recipients not honored)
  • addLabelToChat / removeLabelFromChat — Baileys (addChatLabel / removeChatLabel; WhatsApp-Business-only)
  • getChannelById / subscribeToChannel / unsubscribeFromChannel — Baileys (newsletterMetadata / newsletterFollow / newsletterUnfollow)

Tier 2 — small-to-medium effort, medium-high value

All Tier-2 items wired (see progress above). Remaining channel work is Tier 3: getChannelMessages (raw BinaryNode, no library parser).

Tier 3 — medium effort

# Method : engine Library call to wire Effort Value
13 getChannelMessages : baileys sock.newsletterFetchMessages(jid,count,since,after) (Socket/newsletter.d.ts:19) + hand-written BinaryNodeChannelMessage parser M Read channel posts. The fetch is 1 line; the BinaryNode parsing/normalization is the real work (no library parser exposed).
14 getProducts : baileys sock.getCatalog({jid,limit,cursor}){products, nextPageCursor} (Socket/business.d.ts:7) M Commerce. Derive pagination.total/totalPages (cursor-based, total unknown — approximate or iterate).
15 sendProduct : baileys 2-step: getCatalog lookup (image/title/price) then sendMessage({product:{...},body}) (Types/Message.d.ts:203) M Outbound commerce. productId-only send is not possible without the lookup.
16 getCatalog : baileys getCatalog + getCollections (Socket/business.d.ts:7,11); synthesize partial Catalog (medium-confidence shape) M Catalog metadata. Fields id/description/url have no source — best-effort.

Tier 4 — small effort, lower value

# Method : engine Library call to wire Effort Value
17 getProduct : baileys getCatalog({jid,limit}) then products.find(p=>p.id===productId) (Socket/business.d.ts:7) S Single-product read. Inelegant — loads a whole page to fetch one product. Ship after #14 reuses the same catalog call.

Library limitations — not available without raw-proto/fork effort

These are honestly out of reach of a clean adapter wiring because the installed library exposes no first-class symbol. Listed so operators can plan around them rather than file unactionable bugs.

baileys (9 cells):

  • getSubscribedChannels — no enumerate-newsletters query; all Socket/newsletter.d.ts exports are per-jid. Needs a raw WMex/app-state hack.
  • getLabels / getLabelById / getChatLabels — no label read symbol; only writes (Types/Label.d.ts is types-only). Workaround: capture labels from the messaging-history.set app-state event into an in-memory cache (relay hack, no on-demand refresh).
  • getChatHistory — only fetchMessageHistory (event-delivered sync token); no synchronous per-chat fetchMessages. Needs an OpenWA-side chat-indexed store fed from messages.upsert + messaging-history.set.
  • getMessageReactions — no on-demand fetch; reactions only arrive via the messages.reaction event. Partial local path: persist each event into the messageStore, then read (no historical backfill).
  • getContactStatus / getContactStatusesfetchStatus returns the about text, not 24h stories; stories only surface as status@broadcast messages. Needs an OpenWA-side story accumulator.
  • sendCatalog — no catalog-share message type in AnyMessageContent (only single {product}).

wwjs (5 cells):

  • getCatalog / getProducts / getProduct — no catalog API at all (index.d.ts 0 hits; Product is inbound-only).
  • sendProduct — no outbound product content type.
  • sendCatalog — no outbound catalog content type.

Snapshot summary

  • 71 interface methods, 142 adapter-cells (71 × 2 engines).
  • 111 supported cells; 31 not-available cells across 24 methods.
  • Of the 31 not-available cells: 17 adapter-gaps (fixable) + 14 library-limitations + 0 uncertain.
  • 5 phantom-support corrections in this re-frame (wwjs catalog + status-read methods that stubbed without throwing are now honestly not-available).