|
1 | | -# Plan — Full rewrite of the zaileys Agent Skill suite (provider-aware) |
| 1 | +# Plan — Auto-reject calls (`autoRejectCall` config + `client.rejectCall()` method) |
| 2 | + |
| 3 | +Team ask: auto-reject incoming WhatsApp calls, **toggleable via `Client` config**, and **expose the |
| 4 | +method** so it can be called manually. Then update docs. |
| 5 | + |
| 6 | +## Findings (read-only investigation) |
| 7 | + |
| 8 | +- **Primitive exists**: baileys exposes `rejectCall(callId: string, callFrom: string): Promise<void>` |
| 9 | + (`node_modules/baileys/lib/Socket/index.d.ts:66`). No wrapper in zaileys today (`grep rejectCall src/` = 0). |
| 10 | +- **Call events already wired**: `call-incoming` / `call-ended` emit `CallPayload` |
| 11 | + (`{ callId, from, isGroup, isVideo, timestamp, status }`) — `src/events/decoders/calls.ts`, |
| 12 | + `src/events/pipeline.ts:274`. |
| 13 | +- **Toggle pattern to mirror**: `autoDelete?: AutoDeleteOptions | false` in `ClientOptions` → |
| 14 | + `AutoDeleteSweeper` module in `src/automation/`, constructed in the Client ctor, started on connect, |
| 15 | + stopped on close. `PresenceModule` shows the options+method shape. |
| 16 | +- **Provider dimension (blast radius)**: calls are **WhatsApp-Web-only**. The Cloud API provider has no |
| 17 | + call events, so `rejectCall()` must throw `ZaileysProviderError('UNSUPPORTED_ON_CLOUD')` like |
| 18 | + `edit`/`delete`/`pin`, and `autoRejectCall` must be inert on cloud (never wired). |
| 19 | + |
| 20 | +## Design (decided) |
| 21 | + |
| 22 | +```ts |
| 23 | +// config — OFF by default (rejecting calls is destructive; must be opt-in) |
| 24 | +new Client({ autoRejectCall: true }) |
| 25 | +new Client({ autoRejectCall: { enabled: true, allow: ['628owner@s.whatsapp.net'], onReject: (call) => … } }) |
| 26 | + |
| 27 | +// method — accepts the event payload OR raw ids |
| 28 | +await client.rejectCall(call) // call = CallPayload from 'call-incoming' |
| 29 | +await client.rejectCall(callId, from) |
| 30 | +``` |
2 | 31 |
|
3 | | -Source of truth: [SPEC.md](../SPEC.md). Goal: **full rewrite** of every skill file so both providers |
4 | | -(🔗 unofficial / ☁️ official Cloud API) are native throughout — not appended. Canonical lives in |
5 | | -`skills/*`; `plugins/zaileys-official/skills/*` is generated by `npm run skill:sync` (never hand-edit). |
| 32 | +`AutoRejectCallOptions = { enabled?: boolean; allow?: string[] | ((jid: string) => boolean | Promise<boolean>); onReject?: (call) => void | Promise<void> }` |
| 33 | +— `allow` = whitelist (skip rejecting these callers, mirrors the `citation` predicate style); |
| 34 | +`onReject` = post-reject hook (e.g. send "sorry, calls not supported"). Keep it lean — no |
| 35 | +video/voice/group sub-filters until asked (`isVideo`/`isGroup` are already on the payload, so users |
| 36 | +can filter inside `allow`). |
6 | 37 |
|
7 | | -## Files & dependency graph |
| 38 | +## Dependency graph |
8 | 39 |
|
9 | 40 | ``` |
10 | | - ┌───────────────────────────────┐ |
11 | | - │ T1 references/cloud.md (NEW) │ ← single source of cloud truth |
12 | | - │ T2 references/api.md (+provider)│ everything else cites these |
13 | | - └───────────────┬───────────────┘ |
14 | | - ┌────────────────────────┼────────────────────────┐ |
15 | | - ▼ ▼ ▼ |
16 | | - ┌───────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ |
17 | | - │ T3 assist │ │ T4 errors T5 pitfalls│ │ T8 scaffold │ |
18 | | - │ SKILL.md │ │ T6 recipes T7 trouble │ │ T9 debug T10 review │ |
19 | | - └───────┬───────┘ └───────────┬───────────┘ └──────────┬───────────┘ |
20 | | - └───────────────────────┼──────────────────────────┘ |
21 | | - ▼ |
22 | | - ┌──────────────────────────┐ |
23 | | - │ T11 skill:sync + verify │ ← mirror + diff + snippet audit |
24 | | - └──────────────────────────┘ |
| 41 | + ┌──────────────────────────────────────┐ |
| 42 | + │ T1 automation/auto-reject-call.ts │ module: options, allow-predicate, reject+hook |
| 43 | + │ + unit tests │ (pure, socket injected) |
| 44 | + └───────────────┬──────────────────────┘ |
| 45 | + ▼ |
| 46 | + ┌──────────────────────────────────────┐ |
| 47 | + │ T2 Client wiring │ ClientOptions.autoRejectCall, ctor, attach on |
| 48 | + │ + client.rejectCall() + guard │ connect (baileys only), web-only guard, exports |
| 49 | + └───────────────┬──────────────────────┘ |
| 50 | + ┌───────┴────────┐ |
| 51 | + ▼ ▼ |
| 52 | + ┌───────────────┐ ┌──────────────────┐ |
| 53 | + │ T3 docs site │ │ T4 skill suite │ (independent of each other) |
| 54 | + └───────┬───────┘ └────────┬─────────┘ |
| 55 | + └────────┬──────────┘ |
| 56 | + ▼ |
| 57 | + ┌───────────────┐ |
| 58 | + │ T5 changeset │ minor + release-ready |
| 59 | + └───────────────┘ |
25 | 60 | ``` |
26 | 61 |
|
27 | | -- **T1 (cloud.md) + T2 (api.md provider section) block everything** — they are the referenced truth. |
28 | | -- T3 (orchestrator) references all references incl. cloud.md. |
29 | | -- T4–T7 (references) each cite cloud.md for cloud detail; independent of each other. |
30 | | -- T8–T10 (sibling skills) cite cloud.md + recipes; independent of each other. |
31 | | -- T11 (sync + verify) is last — mirrors canonical to the plugin copy and audits. |
| 62 | +- **T1 blocks T2** (Client constructs the module). |
| 63 | +- **T3 / T4 need T2** (document the shipped API); independent of each other. |
| 64 | +- **T5** last. |
32 | 65 |
|
33 | 66 | ## Phases & checkpoints |
34 | 67 |
|
35 | | -- **Phase A = T1 + T2.** → 🚩 CP1: cloud reference complete + api.md provider section; every snippet valid vs `src/cloud/*` and 4.8.1. |
36 | | -- **Phase B = T3 + T4 + T5 + T6 + T7.** → 🚩 CP2: orchestrator + all 6 references provider-aware; routing covers every cloud intent. |
37 | | -- **Phase C = T8 + T9 + T10.** → 🚩 CP3: scaffold/debug/review all provider-aware. |
38 | | -- **Phase D = T11.** → 🚩 CP4: `diff -r skills plugins/.../skills` clean; ready to commit. |
39 | | - |
40 | | -Slicing note: these are docs, so each task = **one file rewritten completely with both providers**, |
41 | | -ordered by dependency (the referenced `cloud.md` first). Each task is independently verifiable and |
42 | | -leaves the suite coherent. |
| 68 | +- **Phase A = T1 + T2.** → 🚩 CP1: feature works + guarded; full suite green; typecheck clean. |
| 69 | +- **Phase B = T3 + T4.** → 🚩 CP2: docs site builds; skill synced (`diff -r` clean). |
| 70 | +- **Phase C = T5.** → 🚩 CP3: changeset in; ready to release (publish = user's call). |
43 | 71 |
|
44 | 72 | --- |
45 | 73 |
|
46 | 74 | ## Tasks |
47 | 75 |
|
48 | | -### Phase A — Cloud source of truth |
49 | | - |
50 | | -**T1 — `references/cloud.md` (NEW).** The self-contained official-provider reference. |
51 | | -Sections: provider switch + `CloudOptions` (accessToken/phoneNumberId/wabaId/verifyToken/appSecret/ |
52 | | -apiVersion/baseUrl); `connect()` = health-check (no socket/QR); `webhook()` — framework-agnostic |
53 | | -handler + Next/Hono/Express/node:http mounts + Meta dashboard setup; **sending differences** (shared |
54 | | -builder → cite api.md; cloud caveats: buttons max 3, no carousel/poll, `markRead(id,{typing})`, |
55 | | -contact first-name, media sizes); `sendTemplate(to,name,lang,components)`; OTP create+send; marketing |
56 | | -campaigns; **full `wa.cloud.*`** (templates CRUD+get, profile, info, phoneNumbers, flows, commerce |
57 | | -catalogs/products/send, blocklist, qr, analytics, phone, sendAddressRequest); cloud events |
58 | | -(`message-status`/`template-status`/`flow-response`/`order`); the 24-hour window; limits & gotchas |
59 | | -with fixes; `UNSUPPORTED_ON_CLOUD` for web-only. |
60 | | -- *Accept*: every method/param matches `src/cloud/*` (the live-tested surface); no invented API; |
61 | | - web-only features stated as ❌ with the fix. |
62 | | -- *Verify*: grep each `wa.cloud.X`/`send*` against `src/cloud/module.ts`, `transport.ts`, |
63 | | - `client.ts`; snippets compile-plausible. |
64 | | - |
65 | | -**T2 — `references/api.md` provider section.** Add a top-of-file **Providers** subsection (`provider` |
66 | | -option, cloud config shape, "shared vs cloud-only", link to cloud.md). Mark the web-only domain |
67 | | -namespaces (`group`/`newsletter`/`community`/`privacy`/`presence`, `edit`/`delete`/`pin`) as |
68 | | -**unofficial-only — throw `UNSUPPORTED_ON_CLOUD` on cloud**. |
69 | | -- *Accept*: reader knows provider exists before the builder section; web-only surfaces flagged. |
70 | | -- *Verify*: cross-check the guard list against `assertWebProvider` sites in `src/client/client.ts`. |
| 76 | +### T1 — `src/automation/auto-reject-call.ts` + unit tests |
71 | 77 |
|
72 | | -🚩 **CP1** — cloud reference + api provider section accurate & cross-linked. |
| 78 | +Module owning the policy. Socket + logger injected (testable, no Client dependency). |
73 | 79 |
|
74 | | -### Phase B — Orchestrator + references |
75 | | - |
76 | | -**T3 — `zaileys-assist/SKILL.md` full rewrite.** Provider-aware intro ("two providers, one API, |
77 | | -baileys default"); routing table gains rows for cloud/webhook/template/OTP/campaign/Flows/commerce/ |
78 | | -`wa.cloud.*` → cloud.md; mental model adds the provider dimension (cloud = token auth + webhook, no |
79 | | -socket); golden rules provider-aware (cold send needs template; web-only throws on cloud); references |
80 | | -list includes cloud.md. |
81 | | -- *Accept*: any cloud intent routes to cloud.md; baileys stays the default framing. |
82 | | -- *Verify*: every cloud intent in SPEC §2.1 has a routing row. |
83 | | - |
84 | | -**T4 — `references/errors.md`.** Add `ZaileysCloudError` (CONFIG/AUTH/REQUEST_FAILED/RATE_LIMITED/ |
85 | | -NOT_IMPLEMENTED), `ZaileysProviderError` (UNSUPPORTED_ON_CLOUD), + Graph codes (131047, 132000, |
86 | | -131009, 190, 131026, 131056) with cause→fix. Keep existing baileys error classes. |
87 | | -- *Verify*: codes match `src/cloud/errors.ts` + the docs error table. |
| 80 | +```ts |
| 81 | +export interface AutoRejectCallOptions { enabled?: boolean; allow?: …; onReject?: … } |
| 82 | +export interface CallSocketLike { rejectCall(callId: string, callFrom: string): Promise<void> } |
| 83 | +export class AutoRejectCallModule { |
| 84 | + constructor(getSocket: () => CallSocketLike | undefined, options: AutoRejectCallOptions, logger?) |
| 85 | + reject(callId: string, from: string): Promise<void> // raw reject (used by the public method) |
| 86 | + handle(call: CallPayload): Promise<void> // policy: allow-check → reject → onReject |
| 87 | +} |
| 88 | +``` |
88 | 89 |
|
89 | | -**T5 — `references/pitfalls.md`.** Add cloud anti-patterns → correct way: cold send w/o template |
90 | | -(131047); param count mismatch (132000); `rich:true` on cloud (web-only); calling `group`/`newsletter` |
91 | | -on cloud; forgetting the webhook; body-parser eating the raw body; not making inbound idempotent. |
| 90 | +- *Accept*: `handle()` rejects a call when enabled; **skips** when the caller matches `allow` |
| 91 | + (array or predicate); runs `onReject` **after** a successful reject; a throwing `onReject` or |
| 92 | + `rejectCall` is logged, never crashes the client; `reject()` throws a typed error when there's no |
| 93 | + socket (not connected). |
| 94 | +- *Verify*: `tests/automation/auto-reject-call.test.ts` — enabled/disabled, allow-array, allow-predicate |
| 95 | + (sync+async), onReject called once with the payload, reject failure swallowed+logged, no-socket error. |
92 | 96 |
|
93 | | -**T6 — `references/recipes.md`.** Add runnable cloud recipes: cloud echo bot + webhook (Next/Hono/ |
94 | | -Express), send OTP (create+send), marketing campaign (create→approve→broadcast→track), receive |
95 | | -`order`/`flow-response`. |
96 | | -- *Verify*: recipes use only real 4.8.1 API. |
| 97 | +### T2 — Client wiring + public `rejectCall()` + guards |
97 | 98 |
|
98 | | -**T7 — `references/troubleshooting.md`.** Add cloud runtime symptoms: webhook verify 403 / signature |
99 | | -401, events not firing (webhook needs no connect() but must be mounted; check subscribe `messages`), |
100 | | -`131047` outside window, token expired 190. |
| 99 | +- `ClientOptions.autoRejectCall?: boolean | AutoRejectCallOptions` (default **off**), normalized in the |
| 100 | + ctor (`true` → `{ enabled: true }`). |
| 101 | +- Construct the module; **attach to `call-incoming` only on the baileys provider** when enabled |
| 102 | + (wire where the inbound pipeline is attached; detach on close alongside the other handles). |
| 103 | +- Public method `client.rejectCall(callOrId: CallPayload | string, from?: string): Promise<void>` — |
| 104 | + `assertWebProvider('rejectCall')` first (throws `UNSUPPORTED_ON_CLOUD` on cloud), then delegate. |
| 105 | +- Export `AutoRejectCallOptions` from `src/automation/index.ts` (and thus the package root). |
| 106 | +- *Accept*: `new Client({ autoRejectCall: true })` auto-rejects an emitted `call-incoming`; |
| 107 | + default (unset) does **not** reject; `client.rejectCall(call)` and `client.rejectCall(id, from)` both |
| 108 | + work; on `provider:'cloud'` the method throws `UNSUPPORTED_ON_CLOUD` and the config is never wired; |
| 109 | + handlers detach on disconnect (no double-reject after reconnect). |
| 110 | +- *Verify*: `tests/client/auto-reject-call.test.ts` with the existing mock socket |
| 111 | + (`makeIntegrationSocket` + `triggerCall`-style ev emit): toggle on/off, both method arities, cloud |
| 112 | + guard, allow-list end-to-end. Then `pnpm test` (full suite) + `npx tsc --noEmit`. |
101 | 113 |
|
102 | | -🚩 **CP2** — orchestrator + all references provider-aware; routing complete. |
| 114 | +🚩 **CP1** — feature + guard done; 2482+ tests green; typecheck clean. |
103 | 115 |
|
104 | | -### Phase C — Sibling skills |
| 116 | +### T3 — Docs site |
105 | 117 |
|
106 | | -**T8 — `zaileys-scaffold/SKILL.md`.** Ask **which provider** first; scaffold a cloud project variant |
107 | | -(client + `webhook()` route + `.env` for token/phone/verify/appSecret) alongside the baileys variant. |
108 | | -Description mentions cloud/official/webhook. |
| 118 | +- `docs/content/configuration.mdx` — add the `autoRejectCall` row/section (type, default `false`, |
| 119 | + options table, examples incl. `allow` + `onReject`). |
| 120 | +- `docs/content/events.mdx` — under the call events, cross-link auto-reject + `client.rejectCall()`. |
| 121 | +- `docs/content/api-reference.mdx` — add `rejectCall` to the client-methods list. |
| 122 | +- `docs/content/providers.mdx` + `docs/content/official/limits.mdx` — mark calls / `rejectCall` as |
| 123 | + **🔗 unofficial-only** (they don't exist on the Cloud API). |
| 124 | +- *Verify*: `cd docs && npm run build` → clean; `out/index.html` present. |
109 | 125 |
|
110 | | -**T9 — `zaileys-debug/SKILL.md`.** Add cloud error taxonomy + webhook symptoms to the doctor; |
111 | | -description mentions cloud error codes. |
| 126 | +### T4 — Skill suite (canonical `skills/*`, then sync) |
112 | 127 |
|
113 | | -**T10 — `zaileys-review/SKILL.md`.** Add cloud checks: template-gated cold sends, webhook signature + |
114 | | -idempotency, no web-only calls on cloud, secret handling. Description mentions cloud review. |
| 128 | +- `references/api.md` — `autoRejectCall` in the ClientOptions table; `rejectCall` in client methods; |
| 129 | + flag as unofficial-only. |
| 130 | +- `references/recipes.md` — a short "auto-reject calls" recipe (config + manual + notify-caller hook). |
| 131 | +- `zaileys-review/SKILL.md` quick-cues — `rejectCall` in the method list. |
| 132 | +- `npm run skill:sync` → `diff -r skills plugins/zaileys-official/skills` clean. |
| 133 | +- *Verify*: snippets match the shipped API exactly (grep against `src/`); diff clean. |
115 | 134 |
|
116 | | -🚩 **CP3** — all four skills provider-aware. |
| 135 | +🚩 **CP2** — docs build green; skill synced. |
117 | 136 |
|
118 | | -### Phase D — Sync + verify |
| 137 | +### T5 — Changeset |
119 | 138 |
|
120 | | -**T11 — `npm run skill:sync` + verify.** Mirror canonical → plugin copy. `diff -r skills |
121 | | -plugins/zaileys-official/skills` = clean (minus intentional plugin-only files). Audit: every cloud |
122 | | -snippet valid vs `src/cloud/*`; routing covers all cloud intents; unofficial content un-regressed. |
123 | | -- *Verify*: `diff -r` empty; spot-check 5 cloud snippets against source. |
| 139 | +- `.changeset/*.md` **minor** (additive feature): config `autoRejectCall` + `client.rejectCall()`. |
| 140 | +- *Verify*: `npx changeset version` dry-sane (do NOT version/publish without the user's go-ahead). |
124 | 141 |
|
125 | | -🚩 **CP4** — plugin copy matches canonical; ready to commit (release/publish = ask user). |
| 142 | +🚩 **CP3** — ready to release. |
126 | 143 |
|
127 | 144 | ## Risks |
128 | 145 |
|
129 | | -- **Snippet drift** — inventing Cloud-API methods. Mitigate: grep every call against `src/cloud/*` |
130 | | - before writing; T1 is the vetted source all others cite. |
131 | | -- **Sync drift** — hand-editing the plugin copy. Mitigate: edit only `skills/*`, T11 regenerates. |
132 | | -- **Over-duplication** — re-teaching shared builder per provider. Mitigate: cloud.md documents only |
133 | | - differences + cloud-exclusive; cites api.md for shared. |
| 146 | +- **Destructive default** — auto-rejecting calls without opt-in would surprise users. Mitigate: default |
| 147 | + **off**; `true` must be explicit. |
| 148 | +- **Double-reject / leaks after reconnect** — the `call-incoming` handler must be detached with the |
| 149 | + other pipeline handles. Mitigate: attach/detach beside the existing `inboundHandle` lifecycle; test |
| 150 | + reconnect. |
| 151 | +- **Provider drift** — calls are web-only; forgetting the guard means a confusing cloud crash. |
| 152 | + Mitigate: `assertWebProvider` + a cloud-guard test (T2), docs/skill flagged (T3/T4). |
| 153 | +- **Hook failures** — a user `onReject` that throws must not kill the client. Mitigate: catch+log (T1). |
0 commit comments