@wirecat/whatsapp-cli 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/adapter/adapter.d.ts +4 -2
- package/dist/adapter/adapter.d.ts.map +1 -1
- package/dist/adapter/adapter.js +20 -406
- package/dist/adapter/adapter.js.map +1 -1
- package/dist/adapter/channels.d.ts.map +1 -1
- package/dist/adapter/channels.js.map +1 -1
- package/dist/adapter/comments.d.ts +5 -0
- package/dist/adapter/comments.d.ts.map +1 -0
- package/dist/adapter/comments.js +14 -0
- package/dist/adapter/comments.js.map +1 -0
- package/dist/adapter/discovery.d.ts +5 -0
- package/dist/adapter/discovery.d.ts.map +1 -0
- package/dist/adapter/discovery.js +28 -0
- package/dist/adapter/discovery.js.map +1 -0
- package/dist/adapter/errors.d.ts +1 -1
- package/dist/adapter/errors.d.ts.map +1 -1
- package/dist/adapter/errors.js +4 -2
- package/dist/adapter/errors.js.map +1 -1
- package/dist/adapter/groups.d.ts.map +1 -1
- package/dist/adapter/groups.js +1 -1
- package/dist/adapter/groups.js.map +1 -1
- package/dist/adapter/listen.d.ts +11 -0
- package/dist/adapter/listen.d.ts.map +1 -0
- package/dist/adapter/listen.js +264 -0
- package/dist/adapter/listen.js.map +1 -0
- package/dist/adapter/map.d.ts +1 -1
- package/dist/adapter/map.d.ts.map +1 -1
- package/dist/adapter/map.js +13 -22
- package/dist/adapter/map.js.map +1 -1
- package/dist/adapter/polls.d.ts +2 -1
- package/dist/adapter/polls.d.ts.map +1 -1
- package/dist/adapter/polls.js +14 -3
- package/dist/adapter/polls.js.map +1 -1
- package/dist/adapter/privacy.d.ts.map +1 -1
- package/dist/adapter/privacy.js +26 -11
- package/dist/adapter/privacy.js.map +1 -1
- package/dist/adapter/sends.d.ts +14 -0
- package/dist/adapter/sends.d.ts.map +1 -0
- package/dist/adapter/sends.js +156 -0
- package/dist/adapter/sends.js.map +1 -0
- package/dist/adapter/settings.js +4 -1
- package/dist/adapter/settings.js.map +1 -1
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +20 -1
- package/dist/app.js.map +1 -1
- package/dist/bot/adapter.d.ts +18 -0
- package/dist/bot/adapter.d.ts.map +1 -0
- package/dist/bot/adapter.js +74 -0
- package/dist/bot/adapter.js.map +1 -0
- package/dist/bot/generated/definitions.d.ts +3 -0
- package/dist/bot/generated/definitions.d.ts.map +1 -0
- package/dist/bot/generated/definitions.js +9254 -0
- package/dist/bot/generated/definitions.js.map +1 -0
- package/dist/bot/generated/manifest.d.ts +3 -0
- package/dist/bot/generated/manifest.d.ts.map +1 -0
- package/dist/bot/generated/manifest.js +2614 -0
- package/dist/bot/generated/manifest.js.map +1 -0
- package/dist/bot/generated/schemas.d.ts +803 -0
- package/dist/bot/generated/schemas.d.ts.map +1 -0
- package/dist/bot/generated/schemas.js +3052 -0
- package/dist/bot/generated/schemas.js.map +1 -0
- package/dist/bot/generated/types.d.ts +3439 -0
- package/dist/bot/generated/types.d.ts.map +1 -0
- package/dist/bot/generated/types.js +5 -0
- package/dist/bot/generated/types.js.map +1 -0
- package/dist/bot/graph.d.ts +53 -0
- package/dist/bot/graph.d.ts.map +1 -0
- package/dist/bot/graph.js +141 -0
- package/dist/bot/graph.js.map +1 -0
- package/dist/bot/messaging.d.ts +30 -0
- package/dist/bot/messaging.d.ts.map +1 -0
- package/dist/bot/messaging.js +124 -0
- package/dist/bot/messaging.js.map +1 -0
- package/dist/bot/mime.d.ts +3 -0
- package/dist/bot/mime.d.ts.map +1 -0
- package/dist/bot/mime.js +20 -0
- package/dist/bot/mime.js.map +1 -0
- package/dist/bot/webhook.d.ts +13 -0
- package/dist/bot/webhook.d.ts.map +1 -0
- package/dist/bot/webhook.js +148 -0
- package/dist/bot/webhook.js.map +1 -0
- package/dist/commands/bot-templates.d.ts +9 -0
- package/dist/commands/bot-templates.d.ts.map +1 -0
- package/dist/commands/bot-templates.js +141 -0
- package/dist/commands/bot-templates.js.map +1 -0
- package/dist/commands/bot.d.ts +14 -0
- package/dist/commands/bot.d.ts.map +1 -0
- package/dist/commands/bot.js +208 -0
- package/dist/commands/bot.js.map +1 -0
- package/dist/commands/session.d.ts +0 -44
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +15 -141
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -8
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.d.ts +2 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/diagnose.d.ts +6 -41
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/engine/channels.d.ts +1 -0
- package/dist/engine/channels.d.ts.map +1 -1
- package/dist/engine/contract.d.ts +32 -2
- package/dist/engine/contract.d.ts.map +1 -1
- package/dist/engine/contract.js +9 -1
- package/dist/engine/contract.js.map +1 -1
- package/dist/engine/discovery.d.ts +34 -0
- package/dist/engine/discovery.d.ts.map +1 -0
- package/dist/engine/discovery.js +22 -0
- package/dist/engine/discovery.js.map +1 -0
- package/dist/engine/events.d.ts +1 -1
- package/dist/engine/events.d.ts.map +1 -1
- package/dist/engine/events.js.map +1 -1
- package/dist/engine/go-engine.d.ts.map +1 -1
- package/dist/engine/go-engine.js +4 -0
- package/dist/engine/go-engine.js.map +1 -1
- package/dist/messenger.d.ts +4 -1
- package/dist/messenger.d.ts.map +1 -1
- package/dist/messenger.js +8 -10
- package/dist/messenger.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +8 -2
- package/dist/program.js.map +1 -1
- package/dist/session.d.ts +56 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +136 -0
- package/dist/session.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -11
- package/skills/whatsapp-cli/SKILL.md +16 -13
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ You: Yes.
|
|
|
45
45
|
**Early.** `wa` is new, and some of what it does has not yet been checked against real WhatsApp. The
|
|
46
46
|
[backlog](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/dev/BACKLOG.md) lists the checks still to run.
|
|
47
47
|
|
|
48
|
+
More: [compared with other tools](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/compare.md) · [on wirecat.dev](https://wirecat.dev/en/docs/wa/compare)
|
|
49
|
+
|
|
48
50
|
## Quick start
|
|
49
51
|
|
|
50
52
|
```sh
|
|
@@ -117,11 +119,11 @@ Your agent. Your WhatsApp. Your rules.
|
|
|
117
119
|
comes with ready prompts: `/catch-up`, `/review`, `/reply`, `/find` and more.
|
|
118
120
|
|
|
119
121
|
```sh
|
|
120
|
-
wa personal mcp
|
|
122
|
+
wa personal mcp add claude-code # or: wa personal mcp add codex
|
|
121
123
|
wa personal mcp config # the entry for Claude Desktop, Cursor and others
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
The profile can send by default, so `mcp
|
|
126
|
+
The profile can send by default, so `mcp add` stops first. Check the profile's permissions, then run it again with
|
|
125
127
|
`--allow-writes`.
|
|
126
128
|
|
|
127
129
|
More: [the MCP server](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/mcp.md) · [from the browser](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/remote.md)
|
|
@@ -130,6 +132,8 @@ More: [the MCP server](https://github.com/WireCatLabs/whatsapp-cli/blob/main/doc
|
|
|
130
132
|
|
|
131
133
|
- **Your own account, as a linked device.** `wa` links the way WhatsApp Web does. It sees your chats, groups and
|
|
132
134
|
contacts, and speaks as you, not as a business account or a bot.
|
|
135
|
+
- **A business number too, when you have one.** `wa <profile> bot` speaks for a WhatsApp Business number on Meta's
|
|
136
|
+
Cloud API: send, templates, and messages received by webhook through your tunnel ([bot](docs/bot.md)).
|
|
133
137
|
- **Local-first search.** Everything received is kept on your computer. Every read and search answers from there and
|
|
134
138
|
never connects.
|
|
135
139
|
- **Made for agents.** One operation per call, the same JSON shape every time with `--json`, a fixed exit code for
|
|
@@ -252,7 +256,7 @@ changing anything; at `ask` the server shows no form, so set up approval in your
|
|
|
252
256
|
|
|
253
257
|
```sh
|
|
254
258
|
claude mcp add wa -- wa personal mcp # Claude Code, by hand
|
|
255
|
-
wa personal mcp
|
|
259
|
+
wa personal mcp check # check the local MCP server
|
|
256
260
|
```
|
|
257
261
|
|
|
258
262
|
**From the browser.** ChatGPT or Claude in the browser reach `wa personal mcp --http --public-url` behind your tunnel,
|
|
@@ -357,6 +361,7 @@ Every page is in [`docs/`](https://github.com/WireCatLabs/whatsapp-cli/blob/main
|
|
|
357
361
|
| [people](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/people.md) | One person: what the store holds about them, your own names and notes |
|
|
358
362
|
| [mcp](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/mcp.md) | How do I connect Claude Desktop, Cursor or another client without a terminal? |
|
|
359
363
|
| [remote](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/remote.md) | ChatGPT or Claude in the browser, through a login and a tunnel |
|
|
364
|
+
| [bot](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/bot.md) | A WhatsApp Business number on Meta's Cloud API: setup, sending, templates, receiving by webhook |
|
|
360
365
|
| [recipes](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/recipes.md) | What can an agent do for me every day, and how do I run it on a schedule? |
|
|
361
366
|
| [commands](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/commands.md) | Every command, option and exit code — **generated** from the program |
|
|
362
367
|
| [configuration](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/configuration.md) | How do I configure common behaviour? |
|
|
@@ -365,6 +370,7 @@ Every page is in [`docs/`](https://github.com/WireCatLabs/whatsapp-cli/blob/main
|
|
|
365
370
|
| [configuration-reference](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/configuration-reference.md) | Every key, default, scope and variable |
|
|
366
371
|
| [cli-contract](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/cli-contract.md) | Invocation, output, errors, headless runs, bounds and previews |
|
|
367
372
|
| [limits](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/limits.md) | Limits, waits and the one connection per device |
|
|
373
|
+
| [compare](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/compare.md) · [web](https://wirecat.dev/en/docs/wa/compare) | How does `wa` compare with wacli, whatsapp-mcp, mudslide and the Cloud API, and when does another tool fit better? |
|
|
368
374
|
| [diagnostics](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/diagnostics.md) | What did a command do, and what is never recorded? |
|
|
369
375
|
| [troubleshooting](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/troubleshooting.md) | Something does not work: what the screen says, and what to do |
|
|
370
376
|
| [security](https://github.com/WireCatLabs/whatsapp-cli/blob/main/docs/security.md) | What does `wa` keep on my computer, and what can go wrong? |
|
|
@@ -2,12 +2,14 @@ import { type MarkdownFormatting } from "@wirecat/cli-core";
|
|
|
2
2
|
import type { LiveUpdates, MessageEditing, MessageMedia, MessagePins, MessageReactions, MessageStars, MessengerCore, PassiveUpdates, PushedHistory, ReadState, ServerReads, StoredMessages } from "@wirecat/cli-core/cli";
|
|
3
3
|
import { type WhatsAppEngine } from "../engine/contract.js";
|
|
4
4
|
import type { Words } from "./checks.js";
|
|
5
|
+
import { type CommentAdapter } from "./comments.js";
|
|
6
|
+
import { type DiscoveryAdapter } from "./discovery.js";
|
|
5
7
|
import { type GroupAdapter } from "./groups.js";
|
|
6
8
|
import { type PollAdapter } from "./polls.js";
|
|
7
9
|
import { type PrivacyAdapter } from "./privacy.js";
|
|
8
10
|
import { type SettingsAdapter } from "./settings.js";
|
|
9
|
-
export type WhatsAppAdapter = MessengerCore & LiveUpdates & PassiveUpdates & PushedHistory & Pick<MessageEditing, "edit" | "delete" | "forward"> & Pick<MessageMedia, "download"> & MessageReactions & MessageStars & ReadState & Pick<ServerReads, "history"> & MarkdownFormatting & PollAdapter & GroupAdapter & PrivacyAdapter & MessagePins & SettingsAdapter;
|
|
11
|
+
export type WhatsAppAdapter = MessengerCore & LiveUpdates & PassiveUpdates & PushedHistory & Pick<MessageEditing, "edit" | "delete" | "forward"> & Pick<MessageMedia, "download"> & MessageReactions & MessageStars & ReadState & Pick<ServerReads, "history"> & MarkdownFormatting & PollAdapter & GroupAdapter & PrivacyAdapter & MessagePins & SettingsAdapter & DiscoveryAdapter & CommentAdapter;
|
|
10
12
|
export declare const whatsAppAdapter: (engine: WhatsAppEngine, stored?: StoredMessages, words?: Words,
|
|
11
13
|
/** Where a skipped pushed event is said — a warning, so `--quiet` drops it. The diagnostic stream if not given. */
|
|
12
|
-
warn?: (message: string) => void) => WhatsAppAdapter;
|
|
14
|
+
warn?: (message: string) => void, now?: () => number) => WhatsAppAdapter;
|
|
13
15
|
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,kBAAkB,EAIxB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,WAAW,EACX,cAAc,EACf,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE/F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,eAAe,CAAA;AACnE,OAAO,EAAE,KAAK,gBAAgB,EAAoB,MAAM,gBAAgB,CAAA;AAGxE,OAAO,EAAE,KAAK,YAAY,EAAgB,MAAM,aAAa,CAAA;AAI7D,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,YAAY,CAAA;AAC1D,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,cAAc,CAAA;AAElE,OAAO,EAAuB,KAAK,eAAe,EAAkC,MAAM,eAAe,CAAA;AAEzG,MAAM,MAAM,eAAe,GAAG,aAAa,GACzC,WAAW,GACX,cAAc,GACd,aAAa,GACb,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC,GACnD,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAC9B,gBAAgB,GAChB,YAAY,GACZ,SAAS,GACT,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,GAC5B,kBAAkB,GAClB,WAAW,GACX,YAAY,GACZ,cAAc,GACd,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAA;AAoChB,eAAO,MAAM,eAAe,WAClB,cAAc,WACd,cAAc,UACf,KAAK;AACZ,mHAAmH;OAC7G,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,QAC1B,MAAM,MAAM,KAChB,eA+KF,CAAA"}
|
package/dist/adapter/adapter.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { resolve as absolute, extname, join } from "node:path";
|
|
6
2
|
import { CliError } from "@wirecat/cli-base";
|
|
7
3
|
import { bareCheck, parseMarkdownSpans, pickChat, } from "@wirecat/cli-core";
|
|
8
|
-
import { EngineError, OLDER_MAX
|
|
9
|
-
import { channelAdapter, channelHistory, channelWrites, isChannel
|
|
4
|
+
import { EngineError, OLDER_MAX } from "../engine/contract.js";
|
|
5
|
+
import { channelAdapter, channelHistory, channelWrites, isChannel } from "./channels.js";
|
|
6
|
+
import { commentAdapter } from "./comments.js";
|
|
7
|
+
import { discoveryAdapter } from "./discovery.js";
|
|
10
8
|
import { fromEngine } from "./errors.js";
|
|
11
9
|
import { toWhatsApp } from "./format-markdown.js";
|
|
12
10
|
import { groupAdapter } from "./groups.js";
|
|
13
|
-
import {
|
|
11
|
+
import { listenAdapter } from "./listen.js";
|
|
12
|
+
import { toAccount, toChat, toMember, toMessage } from "./map.js";
|
|
14
13
|
import { pinAdapter } from "./pins.js";
|
|
15
|
-
import { pollAdapter
|
|
14
|
+
import { pollAdapter } from "./polls.js";
|
|
16
15
|
import { privacyAdapter } from "./privacy.js";
|
|
16
|
+
import { onDisk, sendAdapter } from "./sends.js";
|
|
17
17
|
import { AFTER_EVERY_MESSAGE, settingsAdapter, settingsWrite } from "./settings.js";
|
|
18
18
|
// WhatsApp addresses a message by its sender and by whether it is the account's own, and the engine keeps no
|
|
19
19
|
// messages, so an edit, a deletion, a reaction or a read receipt looks the message up in cli-core's store first.
|
|
@@ -26,8 +26,6 @@ const NOTHING_STORED = {
|
|
|
26
26
|
};
|
|
27
27
|
// whatsmeow's EditWindow: WhatsApp clients ignore an edit that comes later.
|
|
28
28
|
const EDIT_WINDOW_MS = 20 * 60 * 1000;
|
|
29
|
-
/** Options WhatsApp has no way to honour; `noPreview` is always true, since nothing here builds link previews. */
|
|
30
|
-
const REFUSED = ["threadId", "markup", "sticker", "spoiler", "captionAbove"];
|
|
31
29
|
const translated = async (work) => {
|
|
32
30
|
try {
|
|
33
31
|
return await work;
|
|
@@ -35,117 +33,14 @@ const translated = async (work) => {
|
|
|
35
33
|
catch (error) {
|
|
36
34
|
if (error instanceof EngineError)
|
|
37
35
|
throw fromEngine(error);
|
|
38
|
-
|
|
36
|
+
if (error instanceof CliError)
|
|
37
|
+
throw error;
|
|
38
|
+
const failure = new CliError("provider_error", "WhatsApp engine request failed");
|
|
39
|
+
Object.defineProperty(failure, "cause", { value: error });
|
|
40
|
+
throw failure;
|
|
39
41
|
}
|
|
40
42
|
};
|
|
41
|
-
const hit = (message) => ({ ...message, chatTitle: null });
|
|
42
43
|
const PHONE_CHAT = /^\d+@s\.whatsapp\.net$/;
|
|
43
|
-
const ENDED = {
|
|
44
|
-
logged_out: (words) => fromEngine(new EngineError("authentication_error", `WhatsApp logged this device out — log in again with ${words("session.start")}`, "device.logged-out")),
|
|
45
|
-
replaced: () => fromEngine(new EngineError("provider_unavailable", "another process took over this WhatsApp login; only one may hold it", "device.replaced")),
|
|
46
|
-
banned: (_words, until) => fromEngine(new EngineError("permission_error", "WhatsApp has banned or restricted this account; see the phone", "account.banned", until)),
|
|
47
|
-
outdated: () => fromEngine(new EngineError("provider_unavailable", "WhatsApp no longer accepts this version of wa; update it", "client.outdated")),
|
|
48
|
-
stopped: () => fromEngine(new EngineError("provider_unavailable", "the WhatsApp engine stopped unexpectedly", "engine.stopped")),
|
|
49
|
-
};
|
|
50
|
-
const ENDING = new Set([
|
|
51
|
-
...Object.values(ENDED).map((end) => end(bareCheck).details.reason),
|
|
52
|
-
"engine.stopped-mid-write",
|
|
53
|
-
]);
|
|
54
|
-
/** A failure that means the connection is gone, not that one event was odd: it ends the listening. */
|
|
55
|
-
const ends = (error) => (error instanceof EngineError && ENDING.has(error.reason)) ||
|
|
56
|
-
(error instanceof CliError && ENDING.has(error.details.reason));
|
|
57
|
-
/**
|
|
58
|
-
* Hands each event on until `signal` aborts; a connection WhatsApp ended rejects with why, unless `expected` says this
|
|
59
|
-
* process asked for that end. Events are handled one at a time in arrival order. One whose handling fails is skipped
|
|
60
|
-
* and the next still runs: a stalled queue would keep every later push out of the store until a restart. The first skip
|
|
61
|
-
* is said on `warn` at once, kind and code only, never its content; `drained` settles when the handling caught up and
|
|
62
|
-
* then says how many were skipped if there were more.
|
|
63
|
-
*/
|
|
64
|
-
const listening = (engine, kinds, signal, words, warn, handle, expected = () => false) => {
|
|
65
|
-
let stop = () => { };
|
|
66
|
-
let queue = Promise.resolve();
|
|
67
|
-
let skipped = 0;
|
|
68
|
-
let fail = () => { };
|
|
69
|
-
const run = (kind, work) => {
|
|
70
|
-
queue = queue.then(work).catch((error) => fail(kind, error));
|
|
71
|
-
};
|
|
72
|
-
const done = new Promise((resolve, reject) => {
|
|
73
|
-
fail = (kind, error) => {
|
|
74
|
-
if (ends(error))
|
|
75
|
-
return reject(error instanceof EngineError ? fromEngine(error) : error);
|
|
76
|
-
skipped += 1;
|
|
77
|
-
if (skipped > 1)
|
|
78
|
-
return;
|
|
79
|
-
const code = error instanceof CliError ? error.code : error instanceof Error ? error.name : "error";
|
|
80
|
-
warn(`a pushed ${kind} event could not be handled and was skipped (${code}); the ones after it go on, ` +
|
|
81
|
-
"and any more skips are counted when wa stops listening");
|
|
82
|
-
};
|
|
83
|
-
const unlisten = engine.listen(kinds, (event) => {
|
|
84
|
-
if (event.event === "connection" && expected(event.state))
|
|
85
|
-
return resolve();
|
|
86
|
-
const ended = event.event === "connection" ? ENDED[event.state]?.(words, event.until) : undefined;
|
|
87
|
-
if (ended)
|
|
88
|
-
reject(ended);
|
|
89
|
-
else
|
|
90
|
-
run(event.event, () => handle(event));
|
|
91
|
-
});
|
|
92
|
-
const aborted = () => resolve();
|
|
93
|
-
signal.addEventListener("abort", aborted, { once: true });
|
|
94
|
-
stop = () => {
|
|
95
|
-
unlisten();
|
|
96
|
-
signal.removeEventListener("abort", aborted);
|
|
97
|
-
};
|
|
98
|
-
if (signal.aborted)
|
|
99
|
-
resolve();
|
|
100
|
-
});
|
|
101
|
-
// The caller may still be awaiting the engine's readiness when an ended connection rejects `done`.
|
|
102
|
-
done.catch(() => { });
|
|
103
|
-
const drained = () => queue.then(() => {
|
|
104
|
-
if (skipped > 1)
|
|
105
|
-
warn(`${skipped} pushed events could not be handled and were skipped in all`);
|
|
106
|
-
});
|
|
107
|
-
return { done, stop, drained, run };
|
|
108
|
-
};
|
|
109
|
-
/** Re-keys that arrive this close together go to cli-core as one batch, up to this many. */
|
|
110
|
-
const REKEY_WINDOW_MS = 100;
|
|
111
|
-
const REKEY_BATCH = 500;
|
|
112
|
-
const VIDEO = new Set([".mp4", ".m4v", ".mov"]);
|
|
113
|
-
/** As tg sends them: a photo as a photo, a voice as a voice, a video as a video unless `--as-file`, the rest as files. */
|
|
114
|
-
const uploadKind = ({ kind, name, asFile }) => kind === "photo" || kind === "voice" ? kind : !asFile && VIDEO.has(extname(name).toLowerCase()) ? "video" : "document";
|
|
115
|
-
/** A folder only this user can read, gone when `work` ends: the engine writes a download into it, or reads bytes made in memory. */
|
|
116
|
-
const inPrivateFolder = async (work) => {
|
|
117
|
-
const folder = await mkdtemp(join(tmpdir(), "wa-media-"));
|
|
118
|
-
try {
|
|
119
|
-
return await work(folder);
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
await rm(folder, { recursive: true, force: true });
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
const onDisk = async (source, work) => {
|
|
126
|
-
if (source.kind === "file")
|
|
127
|
-
return work(absolute(source.path));
|
|
128
|
-
return inPrivateFolder(async (folder) => {
|
|
129
|
-
const path = join(folder, "upload");
|
|
130
|
-
await writeFile(path, source.bytes, { mode: 0o600, flag: "wx" });
|
|
131
|
-
return work(path);
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
const allOnDisk = async (files, work, placed = []) => {
|
|
135
|
-
const [next, ...rest] = files;
|
|
136
|
-
if (!next)
|
|
137
|
-
return work(placed);
|
|
138
|
-
return onDisk(next.upload.source, (path) => allOnDisk(rest, work, [...placed, { ...next, path }]));
|
|
139
|
-
};
|
|
140
|
-
const quoted = (message) => ({
|
|
141
|
-
id: message.id,
|
|
142
|
-
senderId: message.senderId,
|
|
143
|
-
senderName: message.senderName,
|
|
144
|
-
timestamp: message.timestamp,
|
|
145
|
-
text: message.text,
|
|
146
|
-
attachments: message.attachments,
|
|
147
|
-
outgoing: message.outgoing ?? null,
|
|
148
|
-
});
|
|
149
44
|
const keyOf = (chatId, message) => ({
|
|
150
45
|
chat: chatId,
|
|
151
46
|
id: message.id,
|
|
@@ -154,7 +49,7 @@ const keyOf = (chatId, message) => ({
|
|
|
154
49
|
});
|
|
155
50
|
export const whatsAppAdapter = (engine, stored = NOTHING_STORED, words = bareCheck,
|
|
156
51
|
/** Where a skipped pushed event is said — a warning, so `--quiet` drops it. The diagnostic stream if not given. */
|
|
157
|
-
warn = (line) => process.stderr.write(`${line}\n`)) => {
|
|
52
|
+
warn = (line) => process.stderr.write(`${line}\n`), now = Date.now) => {
|
|
158
53
|
// A guessed key would make WhatsApp drop the change without a word, so a message the store lacks is refused.
|
|
159
54
|
const held = async (chatId, messageId) => {
|
|
160
55
|
const message = await stored.message(chatId, messageId);
|
|
@@ -162,178 +57,13 @@ warn = (line) => process.stderr.write(`${line}\n`)) => {
|
|
|
162
57
|
throw new CliError("not_found", `that message is not in the local store, and WhatsApp needs its sender to address it; ${words("serve")} keeps the store filled`);
|
|
163
58
|
return message;
|
|
164
59
|
};
|
|
165
|
-
// Set by `session end`: WhatsApp then ends the connection as logged out, which is the answer, not a failure.
|
|
166
|
-
let loggingOut = false;
|
|
167
|
-
// Held re-keys of a running `feed`: a live event may name the new id, and must not reach cli-core before them (§5).
|
|
168
|
-
const holding = new Set();
|
|
169
|
-
const handingOn = (onEvent, signal, expected) => {
|
|
170
|
-
const handed = async (event) => {
|
|
171
|
-
for (const handOn of holding)
|
|
172
|
-
handOn();
|
|
173
|
-
if (event.event === "message")
|
|
174
|
-
onEvent({ event: "message", message: hit(toMessage(event.message)) });
|
|
175
|
-
else if (event.event === "edit")
|
|
176
|
-
onEvent({ event: "edit", message: hit(toMessage(event.message)) });
|
|
177
|
-
else if (event.event === "delete")
|
|
178
|
-
onEvent({
|
|
179
|
-
event: "delete",
|
|
180
|
-
chatId: event.chat,
|
|
181
|
-
chatTitle: null,
|
|
182
|
-
messageId: event.id,
|
|
183
|
-
});
|
|
184
|
-
else if (event.event === "star")
|
|
185
|
-
onEvent({
|
|
186
|
-
event: "star",
|
|
187
|
-
chatId: event.chat,
|
|
188
|
-
chatTitle: null,
|
|
189
|
-
messageId: event.id,
|
|
190
|
-
starred: event.starred,
|
|
191
|
-
});
|
|
192
|
-
else if (event.event === "reaction")
|
|
193
|
-
onEvent({
|
|
194
|
-
event: "person_reaction",
|
|
195
|
-
chatId: event.reaction.chat,
|
|
196
|
-
chatTitle: null,
|
|
197
|
-
messageId: event.reaction.message,
|
|
198
|
-
reaction: toPersonReaction(event.reaction),
|
|
199
|
-
});
|
|
200
|
-
else if (event.event === "receipt")
|
|
201
|
-
onEvent({
|
|
202
|
-
event: "delivery",
|
|
203
|
-
chatId: event.receipt.chat,
|
|
204
|
-
chatTitle: null,
|
|
205
|
-
messageId: event.receipt.id,
|
|
206
|
-
delivery: event.receipt.delivery,
|
|
207
|
-
at: event.receipt.at,
|
|
208
|
-
});
|
|
209
|
-
else if (event.event === "vote") {
|
|
210
|
-
const vote = await toVoteEvent(event.vote, stored);
|
|
211
|
-
if (vote)
|
|
212
|
-
onEvent(vote);
|
|
213
|
-
}
|
|
214
|
-
else if (event.event === "chat_event")
|
|
215
|
-
onEvent({
|
|
216
|
-
event: "chat_event",
|
|
217
|
-
chatId: event.change.chat,
|
|
218
|
-
chatTitle: null,
|
|
219
|
-
change: toChatEvent(event.change),
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
// In arrival order: a vote reads its poll from the store, and nothing after it may overtake it.
|
|
223
|
-
const live = listening(engine, ["message", "edit", "delete", "vote", "reaction", "star", "receipt", "chat_event", "connection"], signal, words, warn, handed, expected);
|
|
224
|
-
return {
|
|
225
|
-
done: live.done,
|
|
226
|
-
stop: async () => {
|
|
227
|
-
live.stop();
|
|
228
|
-
await live.drained();
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
};
|
|
232
|
-
// A push after a re-link carries the path the file had then, and the store may hold the one the phone uploaded
|
|
233
|
-
// again. cli-core keeps a stored ref only when the pushed file has none, so the pushed one is left off when it names
|
|
234
|
-
// the same file; another file keeps its own. Only a message with a ref costs a store read; one that fails hands the
|
|
235
|
-
// message on as pushed, never loses it.
|
|
236
|
-
const withoutKeptRefs = async (message) => {
|
|
237
|
-
if (!message.attachments.some((file) => file.providerRef))
|
|
238
|
-
return message;
|
|
239
|
-
const kept = await stored.message(message.chatId, message.id).catch(() => undefined);
|
|
240
|
-
if (!kept?.attachments.some((file) => file.providerRef))
|
|
241
|
-
return message;
|
|
242
|
-
return {
|
|
243
|
-
...message,
|
|
244
|
-
attachments: message.attachments.map((file, position) => {
|
|
245
|
-
if (!sameFile(file.providerRef, kept.attachments[position]?.providerRef))
|
|
246
|
-
return file;
|
|
247
|
-
const { providerRef: _pushed, ...rest } = file;
|
|
248
|
-
return rest;
|
|
249
|
-
}),
|
|
250
|
-
};
|
|
251
|
-
};
|
|
252
|
-
const pushing = async (onBatch, signal, expected) => {
|
|
253
|
-
// cli-core saves each batch's re-keys apart, so thousands at a serve's start must not come one batch each; a push
|
|
254
|
-
// hands on the held ones before its own, as they arrived.
|
|
255
|
-
let held = [];
|
|
256
|
-
let due;
|
|
257
|
-
const flush = () => {
|
|
258
|
-
clearTimeout(due);
|
|
259
|
-
due = undefined;
|
|
260
|
-
if (held.length === 0)
|
|
261
|
-
return;
|
|
262
|
-
const rekeys = held;
|
|
263
|
-
held = [];
|
|
264
|
-
onBatch({ rekeys });
|
|
265
|
-
};
|
|
266
|
-
// In arrival order: a push waits for its store reads, and a re-key after it must not overtake it.
|
|
267
|
-
const handed = async (event) => {
|
|
268
|
-
if (event.event === "rekey") {
|
|
269
|
-
held.push({ from: event.from, to: event.to });
|
|
270
|
-
if (held.length >= REKEY_BATCH)
|
|
271
|
-
flush();
|
|
272
|
-
else
|
|
273
|
-
due ??= setTimeout(() => pushes.run("rekey", flush), REKEY_WINDOW_MS);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (event.event !== "history")
|
|
277
|
-
return;
|
|
278
|
-
flush();
|
|
279
|
-
const { rekeys, chats, contacts, messages, votes, reactions, events } = event.history;
|
|
280
|
-
onBatch({
|
|
281
|
-
rekeys: rekeys ?? [],
|
|
282
|
-
chats: chats.map(toChatUpdate),
|
|
283
|
-
people: contacts.map(toMember),
|
|
284
|
-
messages: await Promise.all(messages.map((message) => withoutKeptRefs(toMessage(message)))),
|
|
285
|
-
votes: votes.flatMap((vote) => toHistoryVote(vote) ?? []),
|
|
286
|
-
reactions: (reactions ?? []).map((reaction) => ({
|
|
287
|
-
chatId: reaction.chat,
|
|
288
|
-
messageId: reaction.message,
|
|
289
|
-
reaction: toPersonReaction(reaction),
|
|
290
|
-
})),
|
|
291
|
-
events: (events ?? []).map((change) => ({ chatId: change.chat, change: toChatEvent(change) })),
|
|
292
|
-
});
|
|
293
|
-
};
|
|
294
|
-
const pushes = listening(engine, ["history", "rekey", "connection"], signal, words, warn, handed, expected);
|
|
295
|
-
const handOn = () => {
|
|
296
|
-
try {
|
|
297
|
-
flush();
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
pushes.run("rekey", () => Promise.reject(error));
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
holding.add(handOn);
|
|
304
|
-
try {
|
|
305
|
-
await pushes.done;
|
|
306
|
-
}
|
|
307
|
-
finally {
|
|
308
|
-
holding.delete(handOn);
|
|
309
|
-
pushes.stop();
|
|
310
|
-
pushes.run("rekey", flush);
|
|
311
|
-
await pushes.drained();
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
const ownLogout = (state) => state === "logged_out" && loggingOut;
|
|
315
60
|
// A deletion for the account alone, a star and a chat marked read are changes to its synced settings, not messages.
|
|
316
61
|
const synced = settingsWrite(translated, words);
|
|
317
|
-
// The phone's new upload replaces the whole stored ref on `file`, so cli-core saves it and a later download or a
|
|
318
|
-
// forward uses the live path; a second read of the same file in this run uses it too.
|
|
319
|
-
async function* fetched(file, attachment, key) {
|
|
320
|
-
const folder = await mkdtemp(join(tmpdir(), "wa-media-"));
|
|
321
|
-
try {
|
|
322
|
-
const path = join(folder, "file");
|
|
323
|
-
const renewed = await translated(engine.download(attachment, path, key));
|
|
324
|
-
if (renewed) {
|
|
325
|
-
// The old URL names the dead copy; without one, the engine builds it from the new path.
|
|
326
|
-
const { url: _dead, ...kept } = attachment.ref;
|
|
327
|
-
attachment.ref = { ...kept, ...renewed };
|
|
328
|
-
file.providerRef = attachment.ref;
|
|
329
|
-
}
|
|
330
|
-
yield* createReadStream(path);
|
|
331
|
-
}
|
|
332
|
-
finally {
|
|
333
|
-
await rm(folder, { recursive: true, force: true });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
62
|
const core = {
|
|
63
|
+
...discoveryAdapter(engine, translated),
|
|
64
|
+
...commentAdapter(engine, stored, translated),
|
|
65
|
+
...sendAdapter({ engine, stored, held, keyOf, translated, resolve: (chat) => core.resolve(chat) }),
|
|
66
|
+
...listenAdapter({ engine, stored, words, warn, translated }),
|
|
337
67
|
formatMarkdown: async (text) => parseMarkdownSpans(text),
|
|
338
68
|
self: () => engine.self(),
|
|
339
69
|
newSendId: () => `3EB0${randomBytes(9).toString("hex").toUpperCase()}`,
|
|
@@ -370,99 +100,13 @@ warn = (line) => process.stderr.write(`${line}\n`)) => {
|
|
|
370
100
|
const card = await translated(engine.chat(jid));
|
|
371
101
|
return { ...toChat(card.chat), members: card.members?.map(toMember) ?? null };
|
|
372
102
|
},
|
|
373
|
-
send: async (chatId, text, options) => {
|
|
374
|
-
const refused = REFUSED.filter((option) => options[option] !== undefined && options[option] !== false);
|
|
375
|
-
if (refused.length > 0)
|
|
376
|
-
throw new CliError("validation_error", `WhatsApp cannot send with ${refused.join(", ")}`);
|
|
377
|
-
const uploads = options.attachments ?? [];
|
|
378
|
-
const [upload] = uploads;
|
|
379
|
-
const files = uploads.map((one) => ({ upload: one, kind: uploadKind(one) }));
|
|
380
|
-
if (files.length > 1 && files.some(({ kind }) => kind !== "photo" && kind !== "video"))
|
|
381
|
-
throw new CliError("validation_error", "a WhatsApp album holds photos and videos only; send a document in a message of its own");
|
|
382
|
-
if (upload?.kind === "voice" && text.trim() !== "")
|
|
383
|
-
throw new CliError("validation_error", "WhatsApp sends a voice message alone, without text");
|
|
384
|
-
const { stickerFile } = options;
|
|
385
|
-
// Untried on a live channel, where a post carries a URL and no key.
|
|
386
|
-
if (stickerFile && isChannel(chatId))
|
|
387
|
-
throw new CliError("validation_error", "wa does not post stickers to a WhatsApp channel yet");
|
|
388
|
-
if (isChannel(chatId))
|
|
389
|
-
await refuseChannelPost(engine, translated, chatId, {
|
|
390
|
-
files: files.length,
|
|
391
|
-
reply: options.replyTo !== undefined,
|
|
392
|
-
});
|
|
393
|
-
// A reply to a message the store lacks still goes, with its id alone.
|
|
394
|
-
const answered = options.replyTo === undefined ? undefined : await stored.message(chatId, options.replyTo);
|
|
395
|
-
const request = {
|
|
396
|
-
chat: chatId,
|
|
397
|
-
text: options.formatting ? toWhatsApp(text, options.formatting) : text,
|
|
398
|
-
id: options.sendId,
|
|
399
|
-
...(options.replyTo === undefined ? {} : { replyTo: options.replyTo }),
|
|
400
|
-
...(answered
|
|
401
|
-
? { quoted: { fromMe: answered.outgoing === true, sender: answered.senderId, text: answered.text } }
|
|
402
|
-
: {}),
|
|
403
|
-
};
|
|
404
|
-
const message = await translated(files.length > 1
|
|
405
|
-
? allOnDisk(files, (placed) => engine.sendAlbum({
|
|
406
|
-
...request,
|
|
407
|
-
files: placed.map(({ upload, kind, path }) => ({ kind, path, name: upload.name })),
|
|
408
|
-
}))
|
|
409
|
-
: upload
|
|
410
|
-
? onDisk(upload.source, (path) => engine.sendMedia({ ...request, file: { kind: uploadKind(upload), path, name: upload.name } }))
|
|
411
|
-
: stickerFile
|
|
412
|
-
? onDisk(stickerFile.source, (path) => engine.sendMedia({ ...request, file: { kind: "sticker", path, name: stickerFile.name } }))
|
|
413
|
-
: engine.send(request));
|
|
414
|
-
return { message: toMessage(message), sendId: options.sendId };
|
|
415
|
-
},
|
|
416
|
-
forward: async (fromChatId, messageId, toChatId, { sendId, threadId }) => {
|
|
417
|
-
if (threadId !== undefined)
|
|
418
|
-
throw new CliError("validation_error", "WhatsApp forwards without a topic");
|
|
419
|
-
const original = await held(fromChatId, messageId);
|
|
420
|
-
const attachments = original.attachments.map(toWaAttachment);
|
|
421
|
-
const [attachment] = attachments;
|
|
422
|
-
if (attachments.length > 1 || (attachments.length === 1 && !attachment?.ref))
|
|
423
|
-
throw new CliError("validation_error", "the local store does not hold this message's content whole, so it cannot be forwarded; forward it on the phone");
|
|
424
|
-
if (!attachment && original.text === "")
|
|
425
|
-
throw new CliError("validation_error", "that message has nothing to forward");
|
|
426
|
-
// A channel's files carry no key, and a chat's recipient cannot open a file without one.
|
|
427
|
-
if (isChannel(fromChatId) && attachment?.ref && "directPath" in attachment.ref)
|
|
428
|
-
throw new CliError("validation_error", "a WhatsApp channel's file is not encrypted, so a forward would arrive as a file that does not open; " +
|
|
429
|
-
"save it on the phone and send it as a new file", { reason: "media.channel-file-forward" });
|
|
430
|
-
const copy = await translated(engine.forward({ chat: toChatId, id: sendId, text: original.text, attachment: attachment ?? null }));
|
|
431
|
-
return { ...toMessage(copy), forwardedFrom: quoted(original) };
|
|
432
|
-
},
|
|
433
|
-
// From the keys in the store: WhatsApp has no request for an old message. A file that is not one (a place, a
|
|
434
|
-
// contact card) or that the store has no keys for is named in `skipped`. The message's key goes along, so the
|
|
435
|
-
// engine can ask the phone for a file WhatsApp no longer keeps.
|
|
436
|
-
download: async (chat, messageId) => {
|
|
437
|
-
const { id: chatId } = await core.resolve(chat);
|
|
438
|
-
const message = await held(chatId, messageId);
|
|
439
|
-
const files = [];
|
|
440
|
-
const skipped = [];
|
|
441
|
-
message.attachments.forEach((stored, position) => {
|
|
442
|
-
const attachment = toWaAttachment(stored);
|
|
443
|
-
if (!isFile(attachment)) {
|
|
444
|
-
skipped.push(stored.kind);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
const file = {
|
|
448
|
-
kind: attachment.kind,
|
|
449
|
-
...(attachment.name === undefined ? {} : { name: attachment.name }),
|
|
450
|
-
...(attachment.mime === undefined ? {} : { mime: attachment.mime }),
|
|
451
|
-
...(attachment.size === undefined ? {} : { size: attachment.size }),
|
|
452
|
-
position,
|
|
453
|
-
bytes: () => fetched(file, attachment, keyOf(chatId, message)),
|
|
454
|
-
};
|
|
455
|
-
files.push(file);
|
|
456
|
-
});
|
|
457
|
-
return { files, skipped };
|
|
458
|
-
},
|
|
459
103
|
edit: async (chatId, messageId, text, { markup, formatting }) => {
|
|
460
104
|
if (markup !== undefined)
|
|
461
105
|
throw new CliError("validation_error", "WhatsApp cannot edit with markup");
|
|
462
106
|
const original = await held(chatId, messageId);
|
|
463
107
|
if (!original.outgoing)
|
|
464
108
|
throw new CliError("validation_error", "only your own messages can be edited");
|
|
465
|
-
if (
|
|
109
|
+
if (now() - Date.parse(original.timestamp) > EDIT_WINDOW_MS)
|
|
466
110
|
throw new CliError("validation_error", "WhatsApp takes an edit only within 20 minutes of sending");
|
|
467
111
|
const edited = await translated(engine.edit(keyOf(chatId, original), formatting ? toWhatsApp(text, formatting) : text));
|
|
468
112
|
return { ...original, text: edited.text, editedAt: edited.editedAt };
|
|
@@ -524,37 +168,7 @@ warn = (line) => process.stderr.write(`${line}\n`)) => {
|
|
|
524
168
|
const items = older.messages.map(toMessage).sort((one, other) => one.timestamp.localeCompare(other.timestamp));
|
|
525
169
|
return { items, hasMore: older.more };
|
|
526
170
|
},
|
|
527
|
-
logout: () => {
|
|
528
|
-
loggingOut = true;
|
|
529
|
-
return translated(engine.logout());
|
|
530
|
-
},
|
|
531
171
|
close: () => engine.close(),
|
|
532
|
-
watch: async (onEvent, signal, onReady) => {
|
|
533
|
-
const live = handingOn(onEvent, signal);
|
|
534
|
-
try {
|
|
535
|
-
await translated(engine.ready(signal));
|
|
536
|
-
if (!signal.aborted)
|
|
537
|
-
onReady?.();
|
|
538
|
-
await live.done;
|
|
539
|
-
}
|
|
540
|
-
finally {
|
|
541
|
-
await live.stop();
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
// What a one-shot connection receives: WhatsApp hands a queued message to a linked device once, so it is saved
|
|
545
|
-
// here or lost. Never connects; the command's own request does that.
|
|
546
|
-
listen: async (onEvent, signal) => {
|
|
547
|
-
const live = handingOn(onEvent, signal, ownLogout);
|
|
548
|
-
try {
|
|
549
|
-
await live.done;
|
|
550
|
-
}
|
|
551
|
-
finally {
|
|
552
|
-
await live.stop();
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
// The history a one-shot connection is pushed, which WhatsApp likewise sends once. Never connects.
|
|
556
|
-
listenHistory: (onBatch, signal) => pushing(onBatch, signal, ownLogout),
|
|
557
|
-
feed: (onBatch, signal) => pushing(onBatch, signal),
|
|
558
172
|
// Only phone-form one-to-one chats can need a re-key, and in one request: cli-core waits at most 5 s for it.
|
|
559
173
|
storedChats: async (ids, recipients = []) => {
|
|
560
174
|
const phones = ids.filter((id) => PHONE_CHAT.test(id));
|
|
@@ -563,7 +177,7 @@ warn = (line) => process.stderr.write(`${line}\n`)) => {
|
|
|
563
177
|
await translated(engine.storedChats(phones, typed.length > 0 ? typed : undefined));
|
|
564
178
|
},
|
|
565
179
|
};
|
|
566
|
-
const polls = pollAdapter({ engine, stored, held, keyOf, translated });
|
|
180
|
+
const polls = pollAdapter({ engine, stored, held, keyOf, translated, now });
|
|
567
181
|
const groups = groupAdapter({
|
|
568
182
|
engine,
|
|
569
183
|
resolve: core.resolve,
|