16 KiB
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 markednot-availablein 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 consultstatus, 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,unsubscribeFromChannelon Baileys — vianewsletterMetadata('jid'|'invite', …)→NewsletterMetadatamapped toChannel(id/name/description/inviteCode/subscriberCount/picture/verified/createdAt),newsletterFollow(subscribe, invite→jid bridge),newsletterUnfollow(unsubscribe, 1:1).getChannelByIdon 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 rawBinaryNodeof<message_updates>children (newsletter.js:149). The fetch is one line; the real work is walking the children and mapping each toChannelMessage{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 23Socket/newsletter.d.tsexports are per-jid (newsletterMetadatarequires a key;newsletterSubscribers(jid)returns the count of one newsletter). Thenewsletterevent 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/removeLabelFromChaton the Baileys engine — 1:1 tosock.addChatLabel(chatId, labelId)/sock.removeChatLabel(chatId, labelId)(Socket/chats.d.ts:70-71). WhatsApp-Business-only (rejects on personal accounts). Do not useaddLabel(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 inlib/**/*.d.ts(Types/Label.d.tshas only the interface +LabelColorenum;chats.d.ts/business.d.tsexpose 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 toPaginatedProducts. Caveat: cursor-based, sototalis unknown (approximate or iterate).getCatalog(baileys, adapter-gap, medium-confidence).getCatalogreturns a product list + cursor, not the OpenWACatalogmetadata wrapper. Name needsgetCollections(jid)(business.d.ts:11);id/description/urlhave no source — the adapter would synthesize a partial Catalog (productCount=products.length, rest best-effort).getProduct(baileys, adapter-gap, medium-confidence). No directgetProduct(id); callgetCatalog({jid,limit})thenproducts.find(p=>p.id===productId)— loads the whole page to fetch one product.sendProduct(baileys, adapter-gap).AnyRegularMessageContentaccepts{product: WASendableProduct, businessOwnerJid, body}(Types/Message.d.ts:203, built inmessages.js:397). Two-step wiring:getCataloglookup to resolve the product's image/title/price, thensock.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. BaileysAnyMessageContenthas 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.jshas noClient.getCatalog/getProducts/getProduct/sendProduct/sendCatalog(index.d.tsgrep = 0 hits).Product/Orderare inbound-only parsers. The adapter currently stubs the reads tonull/[]without throwing (phantom support).
Status — post / delete
Wired. ✅
postTextStatus/postImageStatus/postVideoStatus+deleteStatuson whatsapp-web.js. Posts route viasendMessage('status@broadcast', …)({ extra: { backgroundColor, fontStyle } }for text;{ caption }for media);deleteStatuscallsrevokeStatusMessage(statusId)(own-status only). Caveat: whatsapp-web.js has no status-recipient arg, soStatusPostOptions.recipientsis not honored on this engine (it broadcasts to the account's status-privacy audience; a one-time warning is logged). The Baileys engine honorsrecipients(statusJidList).
deleteStatus(baileys) caveat. Markedsupported(no throw), but the adapter self-describes itssendMessage(status@broadcast,{delete})revoke shape as empirically unverified (baileys.adapter.ts:909-911) — only posting was live-spiked. May need a fallback toEngineNotSupportedErrorif 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/getContactStatuseson whatsapp-web.js —getBroadcastById(id)/getBroadcasts()flattened toStatus[](contact viabroadcast.getContact(); type fromMessageTypes; 24h TTL). Caveat:Status.typeis thetext|image|videounion — audio/other story types collapse totext.
getContactStatus/getContactStatuses(baileys, library-limitation).fetchStatus(Socket/chats.d.ts:42viaUSyncStatusProtocol) returns the about/profile text line ({status, setAt}), not 24h stories. No story-read getter exists; story broadcasts surface only asstatus@broadcastmessages viamessages.upsert/messaging-history.setevents. Would require OpenWA to accumulatestatus@broadcastmessages itself and project them intoStatus[].
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 isfetchMessageHistory(count, oldestMsgKey, oldestMsgTimestamp)(Socket/business.d.ts:25) — it returns a sync-token string, not messages; the messages are delivered later via themessaging-history.setevent. There is no per-chatfetchMessages(chatId, limit)on the socket. A synchronousPromise<IncomingMessage[]>for one chat would require an OpenWA-side chat-indexed store populated frommessages.upsert+messaging-history.setevents.getMessageReactions(baileys, library-limitation). No on-demand server fetch. Reactions exist only as event-augmented state onWAMessage.reactions(proto.IReaction[]atWAProto/index.d.ts:10623), mutated byupdateMessageWithReactionand surfaced via themessages.reactionevent. The adapter already processesreactionMessageevents (baileys.adapter.ts:1048-1057) and emitsonMessageReaction, but it does not persist.reactionsinto itsmessageStore(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 viachatModify({ deleteForMe }); moved tosupported.
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', …);recipientsnot 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 BinaryNode→ChannelMessage 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; allSocket/newsletter.d.tsexports are per-jid. Needs a raw WMex/app-state hack.getLabels/getLabelById/getChatLabels— no label read symbol; only writes (Types/Label.d.tsis types-only). Workaround: capture labels from themessaging-history.setapp-state event into an in-memory cache (relay hack, no on-demand refresh).getChatHistory— onlyfetchMessageHistory(event-delivered sync token); no synchronous per-chatfetchMessages. Needs an OpenWA-side chat-indexed store fed frommessages.upsert+messaging-history.set.getMessageReactions— no on-demand fetch; reactions only arrive via themessages.reactionevent. Partial local path: persist each event into themessageStore, then read (no historical backfill).getContactStatus/getContactStatuses—fetchStatusreturns the about text, not 24h stories; stories only surface asstatus@broadcastmessages. Needs an OpenWA-side story accumulator.sendCatalog— no catalog-share message type inAnyMessageContent(only single{product}).
wwjs (5 cells):
getCatalog/getProducts/getProduct— no catalog API at all (index.d.ts0 hits;Productis 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).