@openclaw/crabline 0.1.9 → 0.1.10
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 +47 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +21 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +194 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.js +465 -51
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +306 -58
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +5 -0
- package/dist/src/servers/recorder.js +46 -3
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +2 -0
- package/dist/src/servers/signal.js +444 -56
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +5 -0
- package/dist/src/servers/slack.js +515 -49
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +111 -0
- package/dist/src/servers/telegram.js +811 -152
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +12 -7
- package/dist/src/servers/whatsapp.js +269 -107
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +14 -0
- package/dist/src/servers/zalo.js +521 -88
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
|
@@ -1,14 +1,274 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { ProtocolAddress, SessionCipher, SessionRecord } from "libsignal";
|
|
2
3
|
import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, } from "./whatsapp-wire/crypto.js";
|
|
3
4
|
import { decodeBinaryNode, encodeBinaryNode, S_WHATSAPP_NET, } from "./whatsapp-wire/binary-node.js";
|
|
4
5
|
import { decodeHandshakeMessage, encodeHandshakeMessage } from "./whatsapp-wire/handshake.js";
|
|
5
6
|
import { KEY_BUNDLE_TYPE, xmppPreKey, xmppSignedPreKey } from "./whatsapp-wire/signal.js";
|
|
6
7
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8
|
+
import { closeWebSocketServer } from "./websocket.js";
|
|
9
|
+
import { canonicalizeWhatsAppUserCorrelationJid } from "./whatsapp-jid.js";
|
|
7
10
|
// Keep the local server independent from Baileys at runtime. Tests use Baileys
|
|
8
11
|
// as a black-box client to verify this narrow WhatsApp Web wire subset.
|
|
9
12
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
10
13
|
const IV_LENGTH = 12;
|
|
14
|
+
const MAX_PENDING_INBOUND_MESSAGES = 1_000;
|
|
15
|
+
export const MAX_WHATSAPP_NOISE_FRAME_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
export const MAX_WHATSAPP_WEBSOCKET_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
export const MAX_WHATSAPP_WEBSOCKET_MESSAGE_BYTES = 4 * 1024 * 1024;
|
|
18
|
+
export const MAX_WHATSAPP_NOISE_BUFFER_CHUNKS = 1_024;
|
|
19
|
+
export const MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE = 1_024;
|
|
20
|
+
export const WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS = 5_000;
|
|
21
|
+
const MAX_PENDING_WEBSOCKET_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const MAX_PENDING_WEBSOCKET_MESSAGES = 32;
|
|
23
|
+
export const MAX_WHATSAPP_WEBSOCKET_FRAGMENTS = 1_024;
|
|
24
|
+
export const MAX_WHATSAPP_SIGNAL_BUNDLES = 1_024;
|
|
25
|
+
export const MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES = 123;
|
|
11
26
|
const WHATSAPP_NOISE_CERT_CHAIN = Buffer.from("CncKMwjjAhADGiCRKg7Kg1iu4CSulwLBaxX51Tefw6VXGgZqcr5OEbXIRiDQ04bOBijQjZ/TBhJA34Bj82jAHhLpCWBNVBlGnFDieamd8+138S57uMt9ke9mrn5r4+VepwBPKEgHjob6bR70rlCmWDkxZv+CfVjIAxJ2CjIIAxAAGiAcUamsMDmUxsjQuS6hh4pTNHZZnMWZ++o1mX2aqQzOYiCAka6+Bij/3rfcBhJAJw8pRkhTn+1IcOJQVN1OlZg6uikYnCumyO7acFVVX3U3QPXsGSq2TCbCbWrebSC593Su43EgprIDlfU8ZgWFBw==", "base64");
|
|
27
|
+
export function resolveMaxPendingWhatsAppInboundMessages(value) {
|
|
28
|
+
const resolved = value ?? MAX_PENDING_INBOUND_MESSAGES;
|
|
29
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
30
|
+
throw new Error("WhatsApp maxPendingInboundMessages must be a positive safe integer.");
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
export class WhatsAppSignalBundleStore {
|
|
35
|
+
maxBundles;
|
|
36
|
+
#bundles = new Map();
|
|
37
|
+
#lidByPhoneNumber = new Map();
|
|
38
|
+
#sessions = new Map();
|
|
39
|
+
constructor(maxBundles = MAX_WHATSAPP_SIGNAL_BUNDLES) {
|
|
40
|
+
this.maxBundles = maxBundles;
|
|
41
|
+
if (!Number.isSafeInteger(maxBundles) || maxBundles < 1) {
|
|
42
|
+
throw new Error("WhatsApp maxSignalBundles must be a positive safe integer.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
get size() {
|
|
46
|
+
return this.#bundles.size;
|
|
47
|
+
}
|
|
48
|
+
associateLid(phoneNumberJid, lidJid) {
|
|
49
|
+
const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
|
|
50
|
+
const lid = canonicalizeWhatsAppUserCorrelationJid(lidJid);
|
|
51
|
+
if (!phoneNumber?.endsWith("@s.whatsapp.net") || !lid?.endsWith("@lid")) {
|
|
52
|
+
throw new Error("Invalid WhatsApp PN/LID signal mapping.");
|
|
53
|
+
}
|
|
54
|
+
this.#lidByPhoneNumber.set(signalBundleIdentityKey(phoneNumber), signalBundleIdentityKey(lid));
|
|
55
|
+
}
|
|
56
|
+
resolveMany(jids) {
|
|
57
|
+
const uniqueNewIdentities = new Set();
|
|
58
|
+
const identityKeys = [];
|
|
59
|
+
for (const jid of jids) {
|
|
60
|
+
const canonical = canonicalizeWhatsAppUserCorrelationJid(jid);
|
|
61
|
+
if (!canonical) {
|
|
62
|
+
throw new Error(`Invalid WhatsApp signal bundle JID: ${jid}.`);
|
|
63
|
+
}
|
|
64
|
+
const identityKey = signalBundleIdentityKey(canonical);
|
|
65
|
+
identityKeys.push(identityKey);
|
|
66
|
+
if (!this.#bundles.has(identityKey)) {
|
|
67
|
+
uniqueNewIdentities.add(identityKey);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (this.#bundles.size + uniqueNewIdentities.size > this.maxBundles) {
|
|
71
|
+
throw new Error(`WhatsApp signal bundle limit exceeded (${this.maxBundles}).`);
|
|
72
|
+
}
|
|
73
|
+
return identityKeys.map((identityKey) => this.#resolve(identityKey));
|
|
74
|
+
}
|
|
75
|
+
async decryptDirectMessage(params) {
|
|
76
|
+
const recipientJid = canonicalizeWhatsAppUserCorrelationJid(params.recipientJid);
|
|
77
|
+
const recipientIdentityKey = recipientJid ? signalBundleIdentityKey(recipientJid) : undefined;
|
|
78
|
+
const mappedLidIdentityKey = recipientIdentityKey
|
|
79
|
+
? this.#lidByPhoneNumber.get(recipientIdentityKey)
|
|
80
|
+
: undefined;
|
|
81
|
+
const identityKey = mappedLidIdentityKey && this.#bundles.has(mappedLidIdentityKey)
|
|
82
|
+
? mappedLidIdentityKey
|
|
83
|
+
: recipientIdentityKey;
|
|
84
|
+
const bundle = identityKey ? this.#bundles.get(identityKey) : undefined;
|
|
85
|
+
if (!identityKey || !bundle) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const remoteAddress = signalProtocolAddress(params.remoteJid);
|
|
89
|
+
if (!remoteAddress) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const sessions = this.#sessions.get(identityKey) ?? new Map();
|
|
93
|
+
this.#sessions.set(identityKey, sessions);
|
|
94
|
+
const stagedSessions = new Map();
|
|
95
|
+
const storage = {
|
|
96
|
+
async loadSession(id) {
|
|
97
|
+
const session = stagedSessions.get(id) ?? sessions.get(id);
|
|
98
|
+
return session ? cloneSignalSession(session) : undefined;
|
|
99
|
+
},
|
|
100
|
+
async storeSession(id, session) {
|
|
101
|
+
stagedSessions.set(id, cloneSignalSession(session));
|
|
102
|
+
},
|
|
103
|
+
isTrustedIdentity: () => true,
|
|
104
|
+
async loadPreKey(id) {
|
|
105
|
+
if (String(id) !== String(bundle.preKeyId)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return signalKeyPair(bundle.preKey);
|
|
109
|
+
},
|
|
110
|
+
removePreKey: () => undefined,
|
|
111
|
+
loadSignedPreKey: () => signalKeyPair(bundle.signedPreKey.keyPair),
|
|
112
|
+
getOurRegistrationId: () => bundle.registrationId,
|
|
113
|
+
getOurIdentity: () => signalKeyPair(bundle.identityKey),
|
|
114
|
+
};
|
|
115
|
+
const cipher = new SessionCipher(storage, new ProtocolAddress(remoteAddress.name, remoteAddress.deviceId));
|
|
116
|
+
const plaintext = params.type === "pkmsg"
|
|
117
|
+
? await cipher.decryptPreKeyWhisperMessage(params.ciphertext)
|
|
118
|
+
: await cipher.decryptWhisperMessage(params.ciphertext);
|
|
119
|
+
for (const [id, session] of stagedSessions) {
|
|
120
|
+
sessions.set(id, session);
|
|
121
|
+
}
|
|
122
|
+
return plaintext;
|
|
123
|
+
}
|
|
124
|
+
#resolve(bundleKey) {
|
|
125
|
+
const existing = this.#bundles.get(bundleKey);
|
|
126
|
+
if (existing) {
|
|
127
|
+
return existing;
|
|
128
|
+
}
|
|
129
|
+
const identityKey = Curve.generateKeyPair();
|
|
130
|
+
const bundle = {
|
|
131
|
+
identityKey,
|
|
132
|
+
preKey: Curve.generateKeyPair(),
|
|
133
|
+
preKeyId: 1,
|
|
134
|
+
registrationId: 1,
|
|
135
|
+
signedPreKey: signedKeyPair(identityKey, 1),
|
|
136
|
+
};
|
|
137
|
+
this.#bundles.set(bundleKey, bundle);
|
|
138
|
+
return bundle;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function createSerializedMessageHandler(processMessage, onError, options = {}) {
|
|
142
|
+
const maxPendingBytes = options.maxPendingBytes ?? MAX_PENDING_WEBSOCKET_BYTES;
|
|
143
|
+
const maxPendingMessages = options.maxPendingMessages ?? MAX_PENDING_WEBSOCKET_MESSAGES;
|
|
144
|
+
const sizeOf = options.sizeOf ?? (() => 1);
|
|
145
|
+
let failed = false;
|
|
146
|
+
let pendingBytes = 0;
|
|
147
|
+
let pendingMessages = 0;
|
|
148
|
+
let pending = Promise.resolve();
|
|
149
|
+
const fail = (error) => {
|
|
150
|
+
if (!failed) {
|
|
151
|
+
failed = true;
|
|
152
|
+
onError(error);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return (message) => {
|
|
156
|
+
if (failed) {
|
|
157
|
+
return pending;
|
|
158
|
+
}
|
|
159
|
+
const messageBytes = sizeOf(message);
|
|
160
|
+
if (!Number.isSafeInteger(messageBytes) || messageBytes < 0) {
|
|
161
|
+
fail(new Error("WhatsApp WebSocket message size must be a non-negative safe integer."));
|
|
162
|
+
return pending;
|
|
163
|
+
}
|
|
164
|
+
if (pendingMessages >= maxPendingMessages || pendingBytes + messageBytes > maxPendingBytes) {
|
|
165
|
+
fail(new Error("WhatsApp WebSocket inbound backlog limit exceeded."));
|
|
166
|
+
return pending;
|
|
167
|
+
}
|
|
168
|
+
pendingMessages += 1;
|
|
169
|
+
pendingBytes += messageBytes;
|
|
170
|
+
const next = pending
|
|
171
|
+
.then(async () => {
|
|
172
|
+
if (!failed) {
|
|
173
|
+
await processMessage(message);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.finally(() => {
|
|
177
|
+
pendingMessages -= 1;
|
|
178
|
+
pendingBytes -= messageBytes;
|
|
179
|
+
});
|
|
180
|
+
pending = next.catch((error) => {
|
|
181
|
+
fail(error);
|
|
182
|
+
});
|
|
183
|
+
return pending;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export class WhatsAppNoiseFrameDecoder {
|
|
187
|
+
#bufferedBytes = 0;
|
|
188
|
+
#chunks = [];
|
|
189
|
+
#expectIntro = true;
|
|
190
|
+
#offset = 0;
|
|
191
|
+
get bufferedBytes() {
|
|
192
|
+
return this.#bufferedBytes;
|
|
193
|
+
}
|
|
194
|
+
decodeFrames(data) {
|
|
195
|
+
let chunk = rawDataToBuffer(data);
|
|
196
|
+
if (this.#expectIntro) {
|
|
197
|
+
chunk = removeNoiseIntroHeader(chunk);
|
|
198
|
+
this.#expectIntro = false;
|
|
199
|
+
}
|
|
200
|
+
if (chunk.length > 0) {
|
|
201
|
+
if (this.#chunks.length >= MAX_WHATSAPP_NOISE_BUFFER_CHUNKS) {
|
|
202
|
+
throw new Error(`WhatsApp Noise buffer exceeds ${MAX_WHATSAPP_NOISE_BUFFER_CHUNKS} chunks.`);
|
|
203
|
+
}
|
|
204
|
+
this.#chunks.push(chunk);
|
|
205
|
+
this.#bufferedBytes += chunk.length;
|
|
206
|
+
}
|
|
207
|
+
const frames = [];
|
|
208
|
+
while (this.#bufferedBytes >= 3) {
|
|
209
|
+
const size = (this.#peekByte(0) << 16) | (this.#peekByte(1) << 8) | this.#peekByte(2);
|
|
210
|
+
if (size > MAX_WHATSAPP_NOISE_FRAME_BYTES) {
|
|
211
|
+
throw new Error(`WhatsApp Noise frame exceeds ${MAX_WHATSAPP_NOISE_FRAME_BYTES} bytes.`);
|
|
212
|
+
}
|
|
213
|
+
if (this.#bufferedBytes < size + 3) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (frames.length >= MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE) {
|
|
217
|
+
throw new Error(`WhatsApp Noise message exceeds ${MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE} frames.`);
|
|
218
|
+
}
|
|
219
|
+
this.#consume(3);
|
|
220
|
+
frames.push(this.#read(size));
|
|
221
|
+
}
|
|
222
|
+
return frames;
|
|
223
|
+
}
|
|
224
|
+
#consume(length) {
|
|
225
|
+
let remaining = length;
|
|
226
|
+
while (remaining > 0) {
|
|
227
|
+
const chunk = this.#chunks[0];
|
|
228
|
+
if (!chunk) {
|
|
229
|
+
throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
|
|
230
|
+
}
|
|
231
|
+
const available = chunk.length - this.#offset;
|
|
232
|
+
const consumed = Math.min(available, remaining);
|
|
233
|
+
this.#offset += consumed;
|
|
234
|
+
this.#bufferedBytes -= consumed;
|
|
235
|
+
remaining -= consumed;
|
|
236
|
+
if (this.#offset === chunk.length) {
|
|
237
|
+
this.#chunks.shift();
|
|
238
|
+
this.#offset = 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
#peekByte(index) {
|
|
243
|
+
let remaining = index + this.#offset;
|
|
244
|
+
for (const chunk of this.#chunks) {
|
|
245
|
+
if (remaining < chunk.length) {
|
|
246
|
+
return chunk[remaining];
|
|
247
|
+
}
|
|
248
|
+
remaining -= chunk.length;
|
|
249
|
+
}
|
|
250
|
+
throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
|
|
251
|
+
}
|
|
252
|
+
#read(length) {
|
|
253
|
+
const result = Buffer.allocUnsafe(length);
|
|
254
|
+
let resultOffset = 0;
|
|
255
|
+
while (resultOffset < length) {
|
|
256
|
+
const chunk = this.#chunks[0];
|
|
257
|
+
if (!chunk) {
|
|
258
|
+
throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
|
|
259
|
+
}
|
|
260
|
+
const copied = chunk.copy(result, resultOffset, this.#offset, Math.min(chunk.length, this.#offset + length - resultOffset));
|
|
261
|
+
this.#offset += copied;
|
|
262
|
+
this.#bufferedBytes -= copied;
|
|
263
|
+
resultOffset += copied;
|
|
264
|
+
if (this.#offset === chunk.length) {
|
|
265
|
+
this.#chunks.shift();
|
|
266
|
+
this.#offset = 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
12
272
|
class TransportState {
|
|
13
273
|
encKey;
|
|
14
274
|
decKey;
|
|
@@ -31,9 +291,8 @@ class BaileysNoiseServer {
|
|
|
31
291
|
#counter = 0;
|
|
32
292
|
#decKey;
|
|
33
293
|
#encKey;
|
|
34
|
-
#
|
|
294
|
+
#frames = new WhatsAppNoiseFrameDecoder();
|
|
35
295
|
#hash;
|
|
36
|
-
#inBytes = Buffer.alloc(0);
|
|
37
296
|
#salt;
|
|
38
297
|
#serverEphemeralKey;
|
|
39
298
|
#serverStaticKey;
|
|
@@ -47,28 +306,7 @@ class BaileysNoiseServer {
|
|
|
47
306
|
this.#authenticate(NOISE_WA_HEADER);
|
|
48
307
|
}
|
|
49
308
|
decodeFrames(data) {
|
|
50
|
-
|
|
51
|
-
if (this.#expectIntro) {
|
|
52
|
-
chunk = this.#removeIntroHeader(chunk);
|
|
53
|
-
this.#expectIntro = false;
|
|
54
|
-
}
|
|
55
|
-
this.#inBytes = this.#inBytes.length ? Buffer.concat([this.#inBytes, chunk]) : chunk;
|
|
56
|
-
const frames = [];
|
|
57
|
-
while (this.#inBytes.length >= 3) {
|
|
58
|
-
const head0 = this.#inBytes[0];
|
|
59
|
-
const head1 = this.#inBytes[1];
|
|
60
|
-
const head2 = this.#inBytes[2];
|
|
61
|
-
if (head0 === undefined || head1 === undefined || head2 === undefined) {
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
const size = (head0 << 16) | (head1 << 8) | head2;
|
|
65
|
-
if (this.#inBytes.length < size + 3) {
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
frames.push(this.#inBytes.subarray(3, size + 3));
|
|
69
|
-
this.#inBytes = this.#inBytes.subarray(size + 3);
|
|
70
|
-
}
|
|
71
|
-
return frames;
|
|
309
|
+
return this.#frames.decodeFrames(data);
|
|
72
310
|
}
|
|
73
311
|
async decodeTransportNode(frame) {
|
|
74
312
|
if (!this.#transport) {
|
|
@@ -140,66 +378,65 @@ class BaileysNoiseServer {
|
|
|
140
378
|
this.#decKey = readKey;
|
|
141
379
|
this.#counter = 0;
|
|
142
380
|
}
|
|
143
|
-
#removeIntroHeader(chunk) {
|
|
144
|
-
if (chunk.subarray(0, NOISE_WA_HEADER.length).equals(NOISE_WA_HEADER)) {
|
|
145
|
-
return chunk.subarray(NOISE_WA_HEADER.length);
|
|
146
|
-
}
|
|
147
|
-
if (chunk.length >= 11 && chunk.subarray(0, 2).toString("utf8") === "ED" && chunk[3] === 1) {
|
|
148
|
-
const routingInfoPrefix = chunk[4];
|
|
149
|
-
if (routingInfoPrefix === undefined) {
|
|
150
|
-
throw new Error("Invalid Baileys Noise routing header.");
|
|
151
|
-
}
|
|
152
|
-
const routingInfoLength = (routingInfoPrefix << 16) | chunk.readUInt16BE(5);
|
|
153
|
-
const headerLength = 7 + routingInfoLength + NOISE_WA_HEADER.length;
|
|
154
|
-
if (chunk.subarray(headerLength - NOISE_WA_HEADER.length, headerLength).equals(NOISE_WA_HEADER)) {
|
|
155
|
-
return chunk.subarray(headerLength);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
throw new Error("Invalid Baileys Noise intro header.");
|
|
159
|
-
}
|
|
160
381
|
}
|
|
161
382
|
class WhatsAppBaileysWebSocketSession {
|
|
162
383
|
socket;
|
|
163
384
|
params;
|
|
164
385
|
#handshakeState = "client-hello";
|
|
386
|
+
#handleSerializedMessage;
|
|
165
387
|
#noise = new BaileysNoiseServer();
|
|
166
388
|
constructor(socket, params) {
|
|
167
389
|
this.socket = socket;
|
|
168
390
|
this.params = params;
|
|
391
|
+
this.#handleSerializedMessage = createSerializedMessageHandler((data) => this.#handleMessage(data), (error) => {
|
|
392
|
+
const close = resolveWhatsAppWebSocketClose(error);
|
|
393
|
+
this.socket.close(close.code, close.reason);
|
|
394
|
+
}, {
|
|
395
|
+
sizeOf: rawDataByteLength,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
get isOpen() {
|
|
399
|
+
return this.#handshakeState === "open" && this.socket.readyState === WebSocket.OPEN;
|
|
169
400
|
}
|
|
170
401
|
handleMessage(data) {
|
|
171
|
-
void this.#
|
|
172
|
-
this.socket.close(1011, error instanceof Error ? error.message : String(error));
|
|
173
|
-
});
|
|
402
|
+
void this.#handleSerializedMessage(data);
|
|
174
403
|
}
|
|
175
|
-
deliverInboundMessage(message) {
|
|
176
|
-
if (this
|
|
177
|
-
return;
|
|
404
|
+
async deliverInboundMessage(message) {
|
|
405
|
+
if (!this.isOpen) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await this.#sendNode(createInboundMessageNode(message));
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
this.socket.terminate();
|
|
414
|
+
return false;
|
|
178
415
|
}
|
|
179
|
-
this.#sendNode(createInboundMessageNode(message));
|
|
180
416
|
}
|
|
181
417
|
async #handleMessage(data) {
|
|
182
418
|
for (const frame of this.#noise.decodeFrames(data)) {
|
|
183
419
|
if (this.#handshakeState === "client-hello") {
|
|
184
|
-
this.socket
|
|
420
|
+
await sendWhatsAppWebSocketPayload(this.socket, this.#noise.createServerHello(frame));
|
|
185
421
|
this.#handshakeState = "client-finish";
|
|
186
422
|
continue;
|
|
187
423
|
}
|
|
188
424
|
if (this.#handshakeState === "client-finish") {
|
|
189
425
|
this.#noise.finishClientHandshake(frame);
|
|
190
426
|
this.#handshakeState = "open";
|
|
191
|
-
this.#sendNode({
|
|
427
|
+
await this.#sendNode({
|
|
192
428
|
attrs: {
|
|
193
429
|
lid: lidForJid(this.params.selfJid),
|
|
194
430
|
t: unixSeconds(),
|
|
195
431
|
},
|
|
196
432
|
tag: "success",
|
|
197
433
|
});
|
|
198
|
-
this.#sendNode({
|
|
434
|
+
await this.#sendNode({
|
|
199
435
|
attrs: {},
|
|
200
436
|
content: [{ attrs: { count: "0" }, tag: "offline" }],
|
|
201
437
|
tag: "ib",
|
|
202
438
|
});
|
|
439
|
+
this.params.onOpen(this);
|
|
203
440
|
continue;
|
|
204
441
|
}
|
|
205
442
|
await this.#handleNode(await this.#noise.decodeTransportNode(frame));
|
|
@@ -208,19 +445,37 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
208
445
|
async #handleNode(node) {
|
|
209
446
|
await this.#recordNode(node);
|
|
210
447
|
if (node.tag === "iq") {
|
|
211
|
-
this.#sendNode(this.#createIqResult(node));
|
|
448
|
+
await this.#sendNode(this.#createIqResult(node));
|
|
212
449
|
return;
|
|
213
450
|
}
|
|
214
451
|
if (node.tag === "message") {
|
|
215
|
-
|
|
452
|
+
const peer = requireAttr(node, "to");
|
|
453
|
+
const normalizedMessage = await normalizeAcceptedBaileysMessage({
|
|
454
|
+
node,
|
|
455
|
+
remoteJid: this.params.selfJid,
|
|
456
|
+
signalBundles: this.params.signalBundles,
|
|
457
|
+
});
|
|
458
|
+
await this.#sendNode({
|
|
216
459
|
attrs: {
|
|
217
|
-
class: "
|
|
460
|
+
class: "message",
|
|
461
|
+
from: peer,
|
|
218
462
|
id: requireAttr(node, "id"),
|
|
219
|
-
to:
|
|
220
|
-
type:
|
|
463
|
+
to: this.params.selfJid,
|
|
464
|
+
...(node.attrs.type ? { type: node.attrs.type } : {}),
|
|
221
465
|
},
|
|
222
466
|
tag: "ack",
|
|
223
467
|
});
|
|
468
|
+
if (normalizedMessage) {
|
|
469
|
+
await this.params.appendEvent({
|
|
470
|
+
accepted: true,
|
|
471
|
+
at: new Date().toISOString(),
|
|
472
|
+
body: normalizedMessage,
|
|
473
|
+
method: "WEBSOCKET",
|
|
474
|
+
path: this.params.path,
|
|
475
|
+
query: {},
|
|
476
|
+
type: "api",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
224
479
|
}
|
|
225
480
|
}
|
|
226
481
|
#createIqResult(node) {
|
|
@@ -239,7 +494,25 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
239
494
|
return { attrs, content: [{ attrs: {}, tag: "digest" }], tag: "iq" };
|
|
240
495
|
}
|
|
241
496
|
if (node.attrs.xmlns === "encrypt" && child?.tag === "key") {
|
|
242
|
-
|
|
497
|
+
try {
|
|
498
|
+
return { attrs, content: [this.#createKeyList(child)], tag: "iq" };
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
return {
|
|
502
|
+
attrs: { ...attrs, type: "error" },
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
attrs: {
|
|
506
|
+
code: "400",
|
|
507
|
+
text: error instanceof Error ? error.message : String(error),
|
|
508
|
+
type: "modify",
|
|
509
|
+
},
|
|
510
|
+
tag: "error",
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
tag: "iq",
|
|
514
|
+
};
|
|
515
|
+
}
|
|
243
516
|
}
|
|
244
517
|
if (node.attrs.xmlns === "usync" && child?.tag === "usync") {
|
|
245
518
|
return { attrs, content: [this.#createUSyncResult(child)], tag: "iq" };
|
|
@@ -301,6 +574,18 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
301
574
|
};
|
|
302
575
|
}
|
|
303
576
|
if (node.attrs.xmlns === "w:g2") {
|
|
577
|
+
if (node.attrs.type !== "get" || child?.tag !== "query") {
|
|
578
|
+
return {
|
|
579
|
+
attrs: { ...attrs, type: "error" },
|
|
580
|
+
content: [
|
|
581
|
+
{
|
|
582
|
+
attrs: { code: "501", text: "unsupported group operation", type: "cancel" },
|
|
583
|
+
tag: "error",
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
tag: "iq",
|
|
587
|
+
};
|
|
588
|
+
}
|
|
304
589
|
return {
|
|
305
590
|
attrs,
|
|
306
591
|
content: [
|
|
@@ -322,14 +607,15 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
322
607
|
}
|
|
323
608
|
#createKeyList(keyNode) {
|
|
324
609
|
const users = children(keyNode).filter((child) => child.tag === "user");
|
|
610
|
+
const jids = users.map((userNode) => requireAttr(userNode, "jid"));
|
|
611
|
+
const bundles = this.params.signalBundles.resolveMany(jids);
|
|
325
612
|
return {
|
|
326
613
|
attrs: {},
|
|
327
|
-
content:
|
|
614
|
+
content: jids.map((jid, index) => this.#createKeyUser(jid, bundles[index])),
|
|
328
615
|
tag: "list",
|
|
329
616
|
};
|
|
330
617
|
}
|
|
331
|
-
#createKeyUser(jid) {
|
|
332
|
-
const bundle = resolveSignalBundle(this.params.signalBundles, jid);
|
|
618
|
+
#createKeyUser(jid, bundle) {
|
|
333
619
|
return {
|
|
334
620
|
attrs: { jid },
|
|
335
621
|
content: [
|
|
@@ -364,6 +650,10 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
364
650
|
};
|
|
365
651
|
}
|
|
366
652
|
#createUSyncUser(jid) {
|
|
653
|
+
const lid = lidForJid(jid);
|
|
654
|
+
if (canonicalizeWhatsAppUserCorrelationJid(jid)?.endsWith("@s.whatsapp.net")) {
|
|
655
|
+
this.params.signalBundles.associateLid(jid, lid);
|
|
656
|
+
}
|
|
367
657
|
return {
|
|
368
658
|
attrs: { jid },
|
|
369
659
|
content: [
|
|
@@ -378,7 +668,7 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
378
668
|
],
|
|
379
669
|
tag: "devices",
|
|
380
670
|
},
|
|
381
|
-
{ attrs: { val:
|
|
671
|
+
{ attrs: { val: lid }, tag: "lid" },
|
|
382
672
|
],
|
|
383
673
|
tag: "user",
|
|
384
674
|
};
|
|
@@ -393,23 +683,114 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
393
683
|
type: "api",
|
|
394
684
|
});
|
|
395
685
|
}
|
|
396
|
-
#sendNode(node) {
|
|
397
|
-
this.socket
|
|
686
|
+
async #sendNode(node) {
|
|
687
|
+
await sendWhatsAppWebSocketPayload(this.socket, this.#noise.encodeNode(node));
|
|
398
688
|
}
|
|
399
689
|
}
|
|
690
|
+
export async function sendWhatsAppWebSocketPayload(socket, payload) {
|
|
691
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
692
|
+
throw new Error("WhatsApp WebSocket is not open.");
|
|
693
|
+
}
|
|
694
|
+
if (socket.bufferedAmount + payload.byteLength > MAX_WHATSAPP_WEBSOCKET_BUFFERED_BYTES) {
|
|
695
|
+
socket.terminate();
|
|
696
|
+
throw new Error("WhatsApp WebSocket outbound buffer limit exceeded.");
|
|
697
|
+
}
|
|
698
|
+
await new Promise((resolve, reject) => {
|
|
699
|
+
let settled = false;
|
|
700
|
+
const finish = (error) => {
|
|
701
|
+
if (settled) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
settled = true;
|
|
705
|
+
clearTimeout(timeout);
|
|
706
|
+
if (error) {
|
|
707
|
+
reject(error);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
resolve();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const timeout = setTimeout(() => {
|
|
714
|
+
socket.terminate();
|
|
715
|
+
finish(new Error("WhatsApp WebSocket send timed out."));
|
|
716
|
+
}, WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS);
|
|
717
|
+
try {
|
|
718
|
+
socket.send(payload, finish);
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
export function resolveWhatsAppWebSocketClose(error) {
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
+
const code = /(?:backlog|exceeds|limit|payload is too large|too many)/iu.test(message)
|
|
728
|
+
? 1009
|
|
729
|
+
: /(?:baileys|binary node|handshake|noise|protocol|unexpected end|unsupported|invalid)/iu.test(message)
|
|
730
|
+
? 1002
|
|
731
|
+
: 1011;
|
|
732
|
+
return { code, reason: truncateWebSocketCloseReason(message) };
|
|
733
|
+
}
|
|
734
|
+
function truncateWebSocketCloseReason(reason) {
|
|
735
|
+
let result = "";
|
|
736
|
+
for (const character of reason) {
|
|
737
|
+
if (Buffer.byteLength(result + character) > MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES) {
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
result += character;
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
400
744
|
export function attachWhatsAppBaileysWebSocketServer(params) {
|
|
401
|
-
const signalBundles = new
|
|
745
|
+
const signalBundles = new WhatsAppSignalBundleStore();
|
|
402
746
|
const sessions = new Set();
|
|
403
|
-
const
|
|
747
|
+
const pendingMessages = [];
|
|
748
|
+
const maxPendingInboundMessages = resolveMaxPendingWhatsAppInboundMessages(params.maxPendingInboundMessages);
|
|
749
|
+
let pendingReservations = 0;
|
|
750
|
+
let closing = false;
|
|
751
|
+
let flushPromise = Promise.resolve();
|
|
752
|
+
const webSocketServerOptions = {
|
|
753
|
+
maxBufferedChunks: MAX_WHATSAPP_WEBSOCKET_FRAGMENTS,
|
|
754
|
+
maxFragments: MAX_WHATSAPP_WEBSOCKET_FRAGMENTS,
|
|
755
|
+
maxPayload: MAX_WHATSAPP_WEBSOCKET_MESSAGE_BYTES,
|
|
756
|
+
noServer: true,
|
|
757
|
+
};
|
|
758
|
+
const wss = new WebSocketServer(webSocketServerOptions);
|
|
759
|
+
const flushPendingMessages = () => {
|
|
760
|
+
const next = flushPromise.then(async () => {
|
|
761
|
+
while (pendingMessages.length > 0) {
|
|
762
|
+
if (closing) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const message = pendingMessages[0];
|
|
766
|
+
if (!message) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const results = await Promise.all([...sessions].map((session) => session.deliverInboundMessage(message)));
|
|
770
|
+
if (!results.some(Boolean)) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
pendingMessages.shift();
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
flushPromise = next.catch(() => undefined);
|
|
777
|
+
return next;
|
|
778
|
+
};
|
|
779
|
+
const rejectUpgrade = (socket, response) => {
|
|
780
|
+
socket.end(response, () => socket.destroy());
|
|
781
|
+
};
|
|
404
782
|
const handleUpgrade = (request, socket, head) => {
|
|
405
|
-
const url =
|
|
783
|
+
const url = parseWhatsAppWebSocketUpgradeUrl(request.url);
|
|
784
|
+
if (!url) {
|
|
785
|
+
rejectUpgrade(socket, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
406
788
|
if (url.pathname !== params.path) {
|
|
407
789
|
socket.destroy();
|
|
408
790
|
return;
|
|
409
791
|
}
|
|
410
792
|
if (url.searchParams.get("access_token") !== params.accessToken) {
|
|
411
|
-
socket
|
|
412
|
-
socket.destroy();
|
|
793
|
+
rejectUpgrade(socket, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
413
794
|
return;
|
|
414
795
|
}
|
|
415
796
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
@@ -420,40 +801,299 @@ export function attachWhatsAppBaileysWebSocketServer(params) {
|
|
|
420
801
|
wss.on("connection", (socket) => {
|
|
421
802
|
const session = new WhatsAppBaileysWebSocketSession(socket, {
|
|
422
803
|
appendEvent: params.appendEvent,
|
|
804
|
+
onOpen: () => {
|
|
805
|
+
void flushPendingMessages();
|
|
806
|
+
},
|
|
423
807
|
path: params.path,
|
|
424
808
|
selfJid: params.selfJid,
|
|
425
809
|
signalBundles,
|
|
426
810
|
});
|
|
427
811
|
sessions.add(session);
|
|
428
812
|
socket.once("close", () => sessions.delete(session));
|
|
813
|
+
socket.on("error", () => {
|
|
814
|
+
sessions.delete(session);
|
|
815
|
+
socket.terminate();
|
|
816
|
+
});
|
|
429
817
|
socket.on("message", (data) => session.handleMessage(data));
|
|
430
818
|
});
|
|
431
819
|
return {
|
|
432
820
|
async close() {
|
|
821
|
+
closing = true;
|
|
433
822
|
params.httpServer.off("upgrade", handleUpgrade);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
await new Promise((resolve, reject) => {
|
|
438
|
-
wss.close((error) => {
|
|
439
|
-
if (error) {
|
|
440
|
-
reject(error);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
resolve();
|
|
444
|
-
});
|
|
445
|
-
});
|
|
823
|
+
pendingMessages.length = 0;
|
|
824
|
+
await closeWebSocketServer(wss);
|
|
825
|
+
await flushPromise;
|
|
446
826
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
827
|
+
prepareInboundMessage(message) {
|
|
828
|
+
if (pendingMessages.length + pendingReservations >= maxPendingInboundMessages) {
|
|
829
|
+
return undefined;
|
|
450
830
|
}
|
|
831
|
+
let reserved = true;
|
|
832
|
+
let settled = false;
|
|
833
|
+
pendingReservations += 1;
|
|
834
|
+
const releaseReservation = () => {
|
|
835
|
+
if (reserved) {
|
|
836
|
+
reserved = false;
|
|
837
|
+
pendingReservations -= 1;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
return {
|
|
841
|
+
cancel() {
|
|
842
|
+
if (!settled) {
|
|
843
|
+
settled = true;
|
|
844
|
+
releaseReservation();
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
async commit() {
|
|
848
|
+
if (settled) {
|
|
849
|
+
throw new Error("WhatsApp inbound delivery reservation is already settled.");
|
|
850
|
+
}
|
|
851
|
+
settled = true;
|
|
852
|
+
try {
|
|
853
|
+
releaseReservation();
|
|
854
|
+
if (pendingMessages.length >= maxPendingInboundMessages) {
|
|
855
|
+
throw new Error("WhatsApp inbound delivery reservation exceeded queue capacity.");
|
|
856
|
+
}
|
|
857
|
+
pendingMessages.push(message);
|
|
858
|
+
await flushPendingMessages();
|
|
859
|
+
return pendingMessages.includes(message) ? "queued" : "delivered";
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
releaseReservation();
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
};
|
|
451
866
|
},
|
|
452
867
|
};
|
|
453
868
|
}
|
|
869
|
+
export function parseWhatsAppWebSocketUpgradeUrl(requestTarget) {
|
|
870
|
+
try {
|
|
871
|
+
return new URL(requestTarget ?? "/", "http://127.0.0.1");
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function rawDataByteLength(data) {
|
|
878
|
+
if (Array.isArray(data)) {
|
|
879
|
+
return data.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
880
|
+
}
|
|
881
|
+
return data.byteLength;
|
|
882
|
+
}
|
|
883
|
+
function rawDataToBuffer(data) {
|
|
884
|
+
if (Buffer.isBuffer(data)) {
|
|
885
|
+
return data;
|
|
886
|
+
}
|
|
887
|
+
if (Array.isArray(data)) {
|
|
888
|
+
return Buffer.concat(data);
|
|
889
|
+
}
|
|
890
|
+
return Buffer.from(data);
|
|
891
|
+
}
|
|
892
|
+
function removeNoiseIntroHeader(chunk) {
|
|
893
|
+
if (chunk.subarray(0, NOISE_WA_HEADER.length).equals(NOISE_WA_HEADER)) {
|
|
894
|
+
return chunk.subarray(NOISE_WA_HEADER.length);
|
|
895
|
+
}
|
|
896
|
+
if (chunk.length >= 11 && chunk.subarray(0, 2).toString("utf8") === "ED" && chunk[3] === 1) {
|
|
897
|
+
const routingInfoPrefix = chunk[4];
|
|
898
|
+
if (routingInfoPrefix === undefined) {
|
|
899
|
+
throw new Error("Invalid Baileys Noise routing header.");
|
|
900
|
+
}
|
|
901
|
+
const routingInfoLength = (routingInfoPrefix << 16) | chunk.readUInt16BE(5);
|
|
902
|
+
const headerLength = 7 + routingInfoLength + NOISE_WA_HEADER.length;
|
|
903
|
+
if (chunk.subarray(headerLength - NOISE_WA_HEADER.length, headerLength).equals(NOISE_WA_HEADER)) {
|
|
904
|
+
return chunk.subarray(headerLength);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
throw new Error("Invalid Baileys Noise intro header.");
|
|
908
|
+
}
|
|
454
909
|
function children(node) {
|
|
455
910
|
return Array.isArray(node.content) ? node.content : [];
|
|
456
911
|
}
|
|
912
|
+
async function normalizeAcceptedBaileysMessage(params) {
|
|
913
|
+
const peer = canonicalizeWhatsAppUserCorrelationJid(params.node.attrs.to ?? "");
|
|
914
|
+
const messageId = params.node.attrs.id;
|
|
915
|
+
if (!peer || !messageId) {
|
|
916
|
+
return undefined;
|
|
917
|
+
}
|
|
918
|
+
const candidates = encryptedMessageCandidates(params.node);
|
|
919
|
+
const remoteCorrelationJid = canonicalizeWhatsAppUserCorrelationJid(params.remoteJid);
|
|
920
|
+
candidates.sort((left, right) => {
|
|
921
|
+
const leftIsSelf = canonicalizeWhatsAppUserCorrelationJid(left.recipientJid) === remoteCorrelationJid;
|
|
922
|
+
const rightIsSelf = canonicalizeWhatsAppUserCorrelationJid(right.recipientJid) === remoteCorrelationJid;
|
|
923
|
+
return Number(leftIsSelf) - Number(rightIsSelf);
|
|
924
|
+
});
|
|
925
|
+
for (const candidate of candidates) {
|
|
926
|
+
try {
|
|
927
|
+
const decrypted = await params.signalBundles.decryptDirectMessage({
|
|
928
|
+
ciphertext: candidate.ciphertext,
|
|
929
|
+
recipientJid: candidate.recipientJid,
|
|
930
|
+
remoteJid: params.remoteJid,
|
|
931
|
+
type: candidate.type,
|
|
932
|
+
});
|
|
933
|
+
const text = decrypted ? readWhatsAppConversation(unpadRandomMax16(decrypted)) : undefined;
|
|
934
|
+
if (text) {
|
|
935
|
+
return {
|
|
936
|
+
key: {
|
|
937
|
+
fromMe: true,
|
|
938
|
+
id: messageId,
|
|
939
|
+
remoteJid: peer,
|
|
940
|
+
},
|
|
941
|
+
message: { conversation: text },
|
|
942
|
+
messageTimestamp: Math.floor(Date.now() / 1000),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
// Other participant envelopes may not belong to this mock server identity.
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
function encryptedMessageCandidates(node) {
|
|
953
|
+
const result = [];
|
|
954
|
+
const visit = (current, recipientJid) => {
|
|
955
|
+
const nextRecipient = current.tag === "to" ? current.attrs.jid : recipientJid;
|
|
956
|
+
if (current.tag === "enc" &&
|
|
957
|
+
nextRecipient &&
|
|
958
|
+
(current.attrs.type === "msg" || current.attrs.type === "pkmsg") &&
|
|
959
|
+
current.content instanceof Uint8Array) {
|
|
960
|
+
result.push({
|
|
961
|
+
ciphertext: current.content,
|
|
962
|
+
recipientJid: nextRecipient,
|
|
963
|
+
type: current.attrs.type,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
for (const child of children(current)) {
|
|
967
|
+
visit(child, nextRecipient);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
visit(node);
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
function signalKeyPair(pair) {
|
|
974
|
+
return {
|
|
975
|
+
privKey: Buffer.from(pair.private),
|
|
976
|
+
pubKey: pair.public.length === 33
|
|
977
|
+
? Buffer.from(pair.public)
|
|
978
|
+
: Buffer.concat([KEY_BUNDLE_TYPE, pair.public]),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function cloneSignalSession(session) {
|
|
982
|
+
return SessionRecord.deserialize(session.serialize());
|
|
983
|
+
}
|
|
984
|
+
export function signalBundleIdentityKey(jid) {
|
|
985
|
+
return jid.replace(/:\d+(?=@)/u, "");
|
|
986
|
+
}
|
|
987
|
+
function signalProtocolAddress(jid) {
|
|
988
|
+
const match = /^(\d{7,15})(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
|
|
989
|
+
if (!match) {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
const deviceId = Number(match[2] ?? "0");
|
|
993
|
+
if (!Number.isSafeInteger(deviceId) || deviceId < 0) {
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
deviceId,
|
|
998
|
+
name: match[3].toLowerCase() === "lid" ? `${match[1]}_1` : match[1],
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function unpadRandomMax16(value) {
|
|
1002
|
+
const buffer = Buffer.from(value);
|
|
1003
|
+
const padding = buffer.at(-1);
|
|
1004
|
+
if (!padding || padding > 16 || padding > buffer.length) {
|
|
1005
|
+
throw new Error("Invalid WhatsApp message padding.");
|
|
1006
|
+
}
|
|
1007
|
+
for (let index = buffer.length - padding; index < buffer.length; index += 1) {
|
|
1008
|
+
if (buffer[index] !== padding) {
|
|
1009
|
+
throw new Error("Invalid WhatsApp message padding.");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return buffer.subarray(0, buffer.length - padding);
|
|
1013
|
+
}
|
|
1014
|
+
function readWhatsAppConversation(message) {
|
|
1015
|
+
const fields = readProtobufLengthDelimitedFields(message);
|
|
1016
|
+
const conversation = readUtf8(fields.get(1)?.[0]);
|
|
1017
|
+
if (conversation?.trim()) {
|
|
1018
|
+
return conversation;
|
|
1019
|
+
}
|
|
1020
|
+
const extendedText = fields.get(6)?.[0];
|
|
1021
|
+
const extendedConversation = extendedText
|
|
1022
|
+
? readUtf8(readProtobufLengthDelimitedFields(extendedText).get(1)?.[0])
|
|
1023
|
+
: undefined;
|
|
1024
|
+
if (extendedConversation?.trim()) {
|
|
1025
|
+
return extendedConversation;
|
|
1026
|
+
}
|
|
1027
|
+
const deviceSentMessage = fields.get(31)?.[0];
|
|
1028
|
+
const nestedMessage = deviceSentMessage
|
|
1029
|
+
? readProtobufLengthDelimitedFields(deviceSentMessage).get(2)?.[0]
|
|
1030
|
+
: undefined;
|
|
1031
|
+
return nestedMessage ? readWhatsAppConversation(nestedMessage) : undefined;
|
|
1032
|
+
}
|
|
1033
|
+
function readProtobufLengthDelimitedFields(value) {
|
|
1034
|
+
const fields = new Map();
|
|
1035
|
+
let offset = 0;
|
|
1036
|
+
while (offset < value.length) {
|
|
1037
|
+
const tag = readProtobufVarint(value, offset);
|
|
1038
|
+
offset = tag.offset;
|
|
1039
|
+
const fieldNumber = tag.value >>> 3;
|
|
1040
|
+
const wireType = tag.value & 7;
|
|
1041
|
+
if (fieldNumber < 1) {
|
|
1042
|
+
throw new Error("Invalid WhatsApp protobuf field.");
|
|
1043
|
+
}
|
|
1044
|
+
if (wireType === 2) {
|
|
1045
|
+
const length = readProtobufVarint(value, offset);
|
|
1046
|
+
offset = length.offset;
|
|
1047
|
+
const end = offset + length.value;
|
|
1048
|
+
if (end > value.length) {
|
|
1049
|
+
throw new Error("Invalid WhatsApp protobuf length.");
|
|
1050
|
+
}
|
|
1051
|
+
const entries = fields.get(fieldNumber) ?? [];
|
|
1052
|
+
entries.push(value.subarray(offset, end));
|
|
1053
|
+
fields.set(fieldNumber, entries);
|
|
1054
|
+
offset = end;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (wireType === 0) {
|
|
1058
|
+
offset = readProtobufVarint(value, offset).offset;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (wireType === 1) {
|
|
1062
|
+
offset += 8;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (wireType === 5) {
|
|
1066
|
+
offset += 4;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error(`Unsupported WhatsApp protobuf wire type: ${wireType}.`);
|
|
1070
|
+
}
|
|
1071
|
+
return fields;
|
|
1072
|
+
}
|
|
1073
|
+
function readProtobufVarint(value, start) {
|
|
1074
|
+
let result = 0;
|
|
1075
|
+
let shift = 0;
|
|
1076
|
+
let offset = start;
|
|
1077
|
+
while (offset < value.length && shift <= 28) {
|
|
1078
|
+
const byte = value[offset++];
|
|
1079
|
+
result += (byte & 0x7f) * 2 ** shift;
|
|
1080
|
+
if ((byte & 0x80) === 0) {
|
|
1081
|
+
if (!Number.isSafeInteger(result)) {
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
return { offset, value: result };
|
|
1085
|
+
}
|
|
1086
|
+
shift += 7;
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error("Invalid WhatsApp protobuf varint.");
|
|
1089
|
+
}
|
|
1090
|
+
function readUtf8(value) {
|
|
1091
|
+
if (!value) {
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
const text = Buffer.from(value).toString("utf8");
|
|
1095
|
+
return Buffer.from(text, "utf8").equals(Buffer.from(value)) ? text : undefined;
|
|
1096
|
+
}
|
|
457
1097
|
function createInboundMessageNode(message) {
|
|
458
1098
|
const from = message.key.remoteJid;
|
|
459
1099
|
const attrs = {
|
|
@@ -521,22 +1161,6 @@ function lidForJid(jid) {
|
|
|
521
1161
|
const user = jid.split("@", 1)[0]?.split(":", 1)[0] ?? "15550000000";
|
|
522
1162
|
return `${user}@lid`;
|
|
523
1163
|
}
|
|
524
|
-
function resolveSignalBundle(signalBundles, jid) {
|
|
525
|
-
const existing = signalBundles.get(jid);
|
|
526
|
-
if (existing) {
|
|
527
|
-
return existing;
|
|
528
|
-
}
|
|
529
|
-
const identityKey = Curve.generateKeyPair();
|
|
530
|
-
const bundle = {
|
|
531
|
-
identityKey,
|
|
532
|
-
preKey: Curve.generateKeyPair(),
|
|
533
|
-
preKeyId: 1,
|
|
534
|
-
registrationId: 1,
|
|
535
|
-
signedPreKey: signedKeyPair(identityKey, 1),
|
|
536
|
-
};
|
|
537
|
-
signalBundles.set(jid, bundle);
|
|
538
|
-
return bundle;
|
|
539
|
-
}
|
|
540
1164
|
function sanitizeNodeForJson(value) {
|
|
541
1165
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
542
1166
|
return { base64: Buffer.from(value).toString("base64"), type: "Buffer" };
|