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
153 lines
16 KiB
Markdown
153 lines
16 KiB
Markdown
# 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 `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; 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` / `getContactStatuses` — `fetchStatus` 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`).
|