whalibmob 2.0.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 +379 -0
- package/cli.js +509 -0
- package/index.js +35 -0
- package/lib/BinaryNode.js +278 -0
- package/lib/Client.js +957 -0
- package/lib/DeviceManager.js +238 -0
- package/lib/MediaService.js +181 -0
- package/lib/Registration.js +475 -0
- package/lib/Store.js +226 -0
- package/lib/constants.js +68 -0
- package/lib/messages/MessageSender.js +548 -0
- package/lib/noise.js +371 -0
- package/lib/proto/MessageProto.js +229 -0
- package/lib/proto.js +152 -0
- package/lib/signal/SenderKey.js +329 -0
- package/lib/signal/SignalProtocol.js +269 -0
- package/lib/signal/SignalStore.js +161 -0
- package/lib/tokens.js +216 -0
- package/package.json +18 -0
package/lib/noise.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const EventEmitter = require('events');
|
|
6
|
+
const curveJs = require('curve25519-js');
|
|
7
|
+
const { sha256 } = require('@noble/hashes/sha256');
|
|
8
|
+
const { hkdf } = require('@noble/hashes/hkdf');
|
|
9
|
+
const { MOBILE_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT } = require('./constants');
|
|
10
|
+
const { encodeHandshakeClientHello, encodeHandshakeClientFinish,
|
|
11
|
+
decodeServerHello, encodeClientPayload } = require('./proto');
|
|
12
|
+
|
|
13
|
+
// ─── Curve25519 DH helpers ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function stripKeyPrefix(pub) {
|
|
16
|
+
if (pub.length === 33 && pub[0] === 0x05) return pub.slice(1);
|
|
17
|
+
if (pub.length === 32) return pub;
|
|
18
|
+
throw new Error('Invalid DH public key length: ' + pub.length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dhShared(privKey32, pubKey) {
|
|
22
|
+
return Buffer.from(curveJs.sharedKey(privKey32, stripKeyPrefix(pubKey)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Noise_XX_25519_AESGCM_SHA256 ────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0';
|
|
28
|
+
|
|
29
|
+
function hash256(data) {
|
|
30
|
+
return Buffer.from(sha256(data));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function aesgcmEncrypt(key, counter, plaintext, aad) {
|
|
34
|
+
const iv = Buffer.alloc(12);
|
|
35
|
+
iv.writeBigUInt64BE(BigInt(counter), 4);
|
|
36
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
37
|
+
if (aad) cipher.setAAD(aad);
|
|
38
|
+
const enc = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
39
|
+
const tag = cipher.getAuthTag();
|
|
40
|
+
return Buffer.concat([enc, tag]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function aesgcmDecrypt(key, counter, ciphertext, aad) {
|
|
44
|
+
const iv = Buffer.alloc(12);
|
|
45
|
+
iv.writeBigUInt64BE(BigInt(counter), 4);
|
|
46
|
+
const tag = ciphertext.slice(ciphertext.length - 16);
|
|
47
|
+
const data = ciphertext.slice(0, ciphertext.length - 16);
|
|
48
|
+
const dec = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
49
|
+
dec.setAuthTag(tag);
|
|
50
|
+
if (aad) dec.setAAD(aad);
|
|
51
|
+
return Buffer.concat([dec.update(data), dec.final()]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deriveHkdf(salt, ikm, length) {
|
|
55
|
+
return Buffer.from(hkdf(sha256, ikm, salt, new Uint8Array(0), length));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class NoiseState {
|
|
59
|
+
constructor() {
|
|
60
|
+
// Noise spec: if protocol_name.length <= HASHLEN(32), h = protocol_name padded with zeros.
|
|
61
|
+
// Do NOT hash — use the raw 32-byte name directly as the initial h and ck.
|
|
62
|
+
const modeBytes = Buffer.from(NOISE_MODE, 'utf8'); // 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0' = 32 bytes
|
|
63
|
+
this.h = modeBytes;
|
|
64
|
+
this.ck = Buffer.from(modeBytes);
|
|
65
|
+
this.key = null; // single key for both enc+dec during handshake (matches Cobalt)
|
|
66
|
+
this.counter = 0; // single counter for both encrypt+decrypt (matches Cobalt)
|
|
67
|
+
this.hash(MOBILE_PROLOGUE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hash(data) {
|
|
71
|
+
this.h = hash256(Buffer.concat([this.h, data]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mixKey(dhResult) {
|
|
75
|
+
const derived = deriveHkdf(this.ck, dhResult, 64);
|
|
76
|
+
this.ck = derived.slice(0, 32);
|
|
77
|
+
this.key = derived.slice(32, 64);
|
|
78
|
+
this.counter = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
encryptWithAd(plaintext) {
|
|
82
|
+
const ciphertext = aesgcmEncrypt(this.key, this.counter++, plaintext, this.h);
|
|
83
|
+
this.hash(ciphertext);
|
|
84
|
+
return ciphertext;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
decryptWithAd(ciphertext) {
|
|
88
|
+
const plaintext = aesgcmDecrypt(this.key, this.counter++, ciphertext, this.h);
|
|
89
|
+
this.hash(ciphertext);
|
|
90
|
+
return plaintext;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
split() {
|
|
94
|
+
const derived = deriveHkdf(this.ck, Buffer.alloc(0), 64);
|
|
95
|
+
return {
|
|
96
|
+
writeKey: derived.slice(0, 32),
|
|
97
|
+
readKey: derived.slice(32, 64)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Frame helpers ────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
// Cobalt writeRequestHeader: writes 6 bytes (4 for upper part + 2 for lower).
|
|
105
|
+
// Used for ALL outgoing frames (client → server).
|
|
106
|
+
function makeFrame(payload) {
|
|
107
|
+
const len = payload.length;
|
|
108
|
+
const header = Buffer.alloc(6);
|
|
109
|
+
const mss = len >> 16;
|
|
110
|
+
header[0] = (mss >> 24) & 0xff;
|
|
111
|
+
header[1] = (mss >> 16) & 0xff;
|
|
112
|
+
header[2] = (mss >> 8) & 0xff;
|
|
113
|
+
header[3] = mss & 0xff;
|
|
114
|
+
const lss = len & 0xffff;
|
|
115
|
+
header[4] = (lss >> 8) & 0xff;
|
|
116
|
+
header[5] = lss & 0xff;
|
|
117
|
+
return Buffer.concat([header, payload]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Cobalt messageLengthBuffer: always 3 bytes (server → client).
|
|
121
|
+
function readFrameLength(data) {
|
|
122
|
+
if (data.length < 3) return -1;
|
|
123
|
+
return (data[0] << 16) | (data[1] << 8) | data[2];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── NoiseSocket ─────────────────────────────────────────────────────────────
|
|
127
|
+
//
|
|
128
|
+
// Events emitted:
|
|
129
|
+
// 'open' — handshake complete, channel secured
|
|
130
|
+
// 'node'(node) — received a decoded BinaryNode
|
|
131
|
+
// 'close' — TCP connection closed
|
|
132
|
+
// 'error'(err) — an error occurred
|
|
133
|
+
//
|
|
134
|
+
class NoiseSocket extends EventEmitter {
|
|
135
|
+
constructor(store) {
|
|
136
|
+
super();
|
|
137
|
+
this.store = store;
|
|
138
|
+
this.socket = null;
|
|
139
|
+
this.noiseState = null;
|
|
140
|
+
this.writeKey = null;
|
|
141
|
+
this.readKey = null;
|
|
142
|
+
this.writeCounter = 0;
|
|
143
|
+
this.readCounter = 0;
|
|
144
|
+
this.connected = false;
|
|
145
|
+
this.secured = false;
|
|
146
|
+
this._awaitingAuth = false; // true after ClientFinish sent, before server <success>/<failure>
|
|
147
|
+
this.rxBuf = Buffer.alloc(0);
|
|
148
|
+
this._ephemeralKeyPair = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
connect() {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
this._connectResolve = resolve;
|
|
154
|
+
this._connectReject = reject;
|
|
155
|
+
|
|
156
|
+
this.socket = net.createConnection({ host: WHATSAPP_HOST, port: WHATSAPP_PORT });
|
|
157
|
+
this.socket.on('connect', () => this._onTcpConnect());
|
|
158
|
+
this.socket.on('data', (data) => this._onData(data));
|
|
159
|
+
this.socket.on('error', (err) => this._onTcpError(err));
|
|
160
|
+
this.socket.on('close', () => this._onTcpClose());
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_onTcpConnect() {
|
|
165
|
+
this.connected = true;
|
|
166
|
+
this._startHandshake();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_startHandshake() {
|
|
170
|
+
this.noiseState = new NoiseState();
|
|
171
|
+
const seed = crypto.randomBytes(32);
|
|
172
|
+
const kp = curveJs.generateKeyPair(seed);
|
|
173
|
+
this._ephemeralKeyPair = {
|
|
174
|
+
private: Buffer.from(kp.private),
|
|
175
|
+
public: Buffer.from(kp.public)
|
|
176
|
+
};
|
|
177
|
+
this.noiseState.hash(this._ephemeralKeyPair.public);
|
|
178
|
+
|
|
179
|
+
const helloPayload = encodeHandshakeClientHello(this._ephemeralKeyPair.public);
|
|
180
|
+
const frame = Buffer.concat([MOBILE_PROLOGUE, makeFrame(helloPayload)]);
|
|
181
|
+
this.socket.write(frame);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_onData(data) {
|
|
185
|
+
this.rxBuf = Buffer.concat([this.rxBuf, data]);
|
|
186
|
+
this._processBuffer();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_processBuffer() {
|
|
190
|
+
while (true) {
|
|
191
|
+
if (this.rxBuf.length < 3) break;
|
|
192
|
+
const frameLen = readFrameLength(this.rxBuf);
|
|
193
|
+
if (this.rxBuf.length < 3 + frameLen) break;
|
|
194
|
+
const frame = this.rxBuf.slice(3, 3 + frameLen);
|
|
195
|
+
this.rxBuf = this.rxBuf.slice(3 + frameLen);
|
|
196
|
+
|
|
197
|
+
if (!this.secured) {
|
|
198
|
+
this._handleHandshakeFrame(frame);
|
|
199
|
+
} else {
|
|
200
|
+
this._handleSecuredFrame(frame);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_handleHandshakeFrame(frame) {
|
|
206
|
+
try {
|
|
207
|
+
const serverHello = decodeServerHello(frame);
|
|
208
|
+
if (!serverHello.ephemeral) throw new Error('No server ephemeral in ServerHello');
|
|
209
|
+
|
|
210
|
+
this.noiseState.hash(serverHello.ephemeral);
|
|
211
|
+
const sharedEE = dhShared(this._ephemeralKeyPair.private, serverHello.ephemeral);
|
|
212
|
+
this.noiseState.mixKey(sharedEE);
|
|
213
|
+
|
|
214
|
+
const serverStaticDec = this.noiseState.decryptWithAd(serverHello.staticEnc);
|
|
215
|
+
const sharedSE = dhShared(this._ephemeralKeyPair.private, serverStaticDec);
|
|
216
|
+
this.noiseState.mixKey(sharedSE);
|
|
217
|
+
|
|
218
|
+
if (serverHello.payload) {
|
|
219
|
+
this.noiseState.decryptWithAd(serverHello.payload);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const noisePriv = this.store.noiseKeyPair.private;
|
|
223
|
+
// Send the raw 32-byte noise public key (no 0x05 Signal prefix) — matches Baileys/WA protocol
|
|
224
|
+
const noisePub = stripKeyPrefix(this.store.noiseKeyPair.public);
|
|
225
|
+
const encStatic = this.noiseState.encryptWithAd(noisePub);
|
|
226
|
+
const sharedSS = dhShared(noisePriv, serverHello.ephemeral);
|
|
227
|
+
this.noiseState.mixKey(sharedSS);
|
|
228
|
+
|
|
229
|
+
const payload = encodeClientPayload({
|
|
230
|
+
username: BigInt(this.store.phoneNumber),
|
|
231
|
+
passive: false,
|
|
232
|
+
pushName: this.store.name || 'User',
|
|
233
|
+
shortConnect: true,
|
|
234
|
+
connectType: 1,
|
|
235
|
+
connectReason: 1,
|
|
236
|
+
connectAttemptCount: 0,
|
|
237
|
+
device: 0,
|
|
238
|
+
oc: false,
|
|
239
|
+
userAgent: {
|
|
240
|
+
platform: 1,
|
|
241
|
+
version: this.store.version,
|
|
242
|
+
mcc: '000',
|
|
243
|
+
mnc: '000',
|
|
244
|
+
osVersion: this.store.device.osVersion,
|
|
245
|
+
manufacturer: this.store.device.manufacturer,
|
|
246
|
+
device: this.store.device.model,
|
|
247
|
+
osBuildNumber: this.store.device.osBuildNumber,
|
|
248
|
+
phoneId: this.store.fdid.toUpperCase(),
|
|
249
|
+
releaseChannel: 0,
|
|
250
|
+
localeLanguage: 'en',
|
|
251
|
+
localeCountry: 'US',
|
|
252
|
+
deviceType: 0,
|
|
253
|
+
deviceModelType: this.store.device.modelId
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const encPayload = this.noiseState.encryptWithAd(payload);
|
|
258
|
+
const finish = encodeHandshakeClientFinish(encStatic, encPayload);
|
|
259
|
+
this.socket.write(makeFrame(finish));
|
|
260
|
+
|
|
261
|
+
const keys = this.noiseState.split();
|
|
262
|
+
this.writeKey = keys.writeKey;
|
|
263
|
+
this.readKey = keys.readKey;
|
|
264
|
+
this.writeCounter = 0;
|
|
265
|
+
this.readCounter = 0;
|
|
266
|
+
this.secured = true;
|
|
267
|
+
|
|
268
|
+
// Wait for the server's <success> or <failure> confirmation before
|
|
269
|
+
// declaring the channel open. _processBuffer() will call
|
|
270
|
+
// _handleSecuredFrame() for the next frame, which handles auth.
|
|
271
|
+
this._awaitingAuth = true;
|
|
272
|
+
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (this._connectReject) {
|
|
275
|
+
this._connectReject(err);
|
|
276
|
+
this._connectResolve = null;
|
|
277
|
+
this._connectReject = null;
|
|
278
|
+
}
|
|
279
|
+
this.emit('error', err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_handleSecuredFrame(frame) {
|
|
284
|
+
try {
|
|
285
|
+
const plain = aesgcmDecrypt(this.readKey, this.readCounter++, frame, null);
|
|
286
|
+
const { decodeNode } = require('./BinaryNode');
|
|
287
|
+
const node = decodeNode(plain);
|
|
288
|
+
|
|
289
|
+
// ── Auth confirmation from server ─────────────────────────────────────
|
|
290
|
+
// The very first secured frame must be <success> or <failure>.
|
|
291
|
+
// Only after <success> do we resolve the connect() promise and emit 'open'.
|
|
292
|
+
if (this._awaitingAuth) {
|
|
293
|
+
this._awaitingAuth = false;
|
|
294
|
+
const tag = node && node.description;
|
|
295
|
+
|
|
296
|
+
if (tag === 'success') {
|
|
297
|
+
if (this._connectResolve) {
|
|
298
|
+
this._connectResolve();
|
|
299
|
+
this._connectResolve = null;
|
|
300
|
+
this._connectReject = null;
|
|
301
|
+
}
|
|
302
|
+
// Pass the full success node so Client can extract ADV identity,
|
|
303
|
+
// platform, expiration, and other server-provided account info.
|
|
304
|
+
this.emit('open', node);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (tag === 'failure') {
|
|
309
|
+
const attrs = (node && node.attrs) || {};
|
|
310
|
+
const reason = attrs.reason || attrs.location || '401';
|
|
311
|
+
const err = new Error('WhatsApp auth failure: ' + reason);
|
|
312
|
+
err.code = reason;
|
|
313
|
+
if (this._connectReject) {
|
|
314
|
+
this._connectReject(err);
|
|
315
|
+
this._connectResolve = null;
|
|
316
|
+
this._connectReject = null;
|
|
317
|
+
}
|
|
318
|
+
this.emit('error', err);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Unexpected first node — log it as an error but try to continue
|
|
323
|
+
const err = new Error('Unexpected first server node after handshake: ' + tag);
|
|
324
|
+
if (this._connectReject) {
|
|
325
|
+
this._connectReject(err);
|
|
326
|
+
this._connectResolve = null;
|
|
327
|
+
this._connectReject = null;
|
|
328
|
+
}
|
|
329
|
+
this.emit('error', err);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
this.emit('node', node);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
this.emit('error', err);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
sendNode(node) {
|
|
341
|
+
if (!this.secured) throw new Error('Channel not secured — handshake incomplete');
|
|
342
|
+
const { encodeNode } = require('./BinaryNode');
|
|
343
|
+
const plain = encodeNode(node);
|
|
344
|
+
const ciphertext = aesgcmEncrypt(this.writeKey, this.writeCounter++, plain, null);
|
|
345
|
+
this.socket.write(makeFrame(ciphertext));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_onTcpError(err) {
|
|
349
|
+
if (this._connectReject) {
|
|
350
|
+
this._connectReject(err);
|
|
351
|
+
this._connectResolve = null;
|
|
352
|
+
this._connectReject = null;
|
|
353
|
+
}
|
|
354
|
+
this.emit('error', err);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_onTcpClose() {
|
|
358
|
+
this.connected = false;
|
|
359
|
+
this.secured = false;
|
|
360
|
+
this.emit('close');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
close() {
|
|
364
|
+
if (this.socket) {
|
|
365
|
+
try { this.socket.destroy(); } catch (_) {}
|
|
366
|
+
this.socket = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = { NoiseSocket };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WIRE_VARINT = 0;
|
|
4
|
+
const WIRE_LEN = 2;
|
|
5
|
+
|
|
6
|
+
function tag(fieldNumber, wireType) {
|
|
7
|
+
return encodeVarint((fieldNumber << 3) | wireType);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function encodeVarint(n) {
|
|
11
|
+
const bytes = [];
|
|
12
|
+
let val = n >>> 0;
|
|
13
|
+
while (val > 127) {
|
|
14
|
+
bytes.push((val & 0x7f) | 0x80);
|
|
15
|
+
val = val >>> 7;
|
|
16
|
+
}
|
|
17
|
+
bytes.push(val);
|
|
18
|
+
return Buffer.from(bytes);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function encodeVarintSigned(n) {
|
|
22
|
+
if (n < 0) {
|
|
23
|
+
const big = BigInt(n);
|
|
24
|
+
const unsigned = big & 0xffffffffffffffffn;
|
|
25
|
+
const bytes = [];
|
|
26
|
+
let val = unsigned;
|
|
27
|
+
while (val > 127n) {
|
|
28
|
+
bytes.push(Number(val & 0x7fn) | 0x80);
|
|
29
|
+
val = val >> 7n;
|
|
30
|
+
}
|
|
31
|
+
bytes.push(Number(val));
|
|
32
|
+
return Buffer.from(bytes);
|
|
33
|
+
}
|
|
34
|
+
return encodeVarint(n);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function field(fieldNum, wireType, data) {
|
|
38
|
+
if (data === null || data === undefined) return Buffer.alloc(0);
|
|
39
|
+
const t = tag(fieldNum, wireType);
|
|
40
|
+
if (wireType === WIRE_VARINT) {
|
|
41
|
+
return Buffer.concat([t, data]);
|
|
42
|
+
}
|
|
43
|
+
const lenBuf = encodeVarint(data.length);
|
|
44
|
+
return Buffer.concat([t, lenBuf, data]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function varint(n) { return encodeVarint(n); }
|
|
48
|
+
function varintS(n) { return encodeVarintSigned(n); }
|
|
49
|
+
function bytes(b) { return b ? Buffer.from(b) : Buffer.alloc(0); }
|
|
50
|
+
function str(s) { return s ? Buffer.from(s, 'utf8') : Buffer.alloc(0); }
|
|
51
|
+
function bool(b) { return encodeVarint(b ? 1 : 0); }
|
|
52
|
+
|
|
53
|
+
function encodeText(text, contextInfo) {
|
|
54
|
+
if (contextInfo) {
|
|
55
|
+
const inner = Buffer.concat([
|
|
56
|
+
field(1, WIRE_LEN, str(text)),
|
|
57
|
+
field(17, WIRE_LEN, encodeContextInfo(contextInfo))
|
|
58
|
+
]);
|
|
59
|
+
return field(38, WIRE_LEN, inner);
|
|
60
|
+
}
|
|
61
|
+
return field(1, WIRE_LEN, str(text));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function encodeContextInfo(ctx) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (ctx.quotedMessageId) parts.push(field(1, WIRE_LEN, str(ctx.quotedMessageId)));
|
|
67
|
+
if (ctx.participant) parts.push(field(2, WIRE_LEN, str(ctx.participant)));
|
|
68
|
+
if (ctx.quotedMessage) parts.push(field(3, WIRE_LEN, ctx.quotedMessage));
|
|
69
|
+
if (ctx.remoteJid) parts.push(field(4, WIRE_LEN, str(ctx.remoteJid)));
|
|
70
|
+
return Buffer.concat(parts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function encodeImageMessage(opts) {
|
|
74
|
+
const parts = [
|
|
75
|
+
field(1, WIRE_LEN, str(opts.url)),
|
|
76
|
+
field(2, WIRE_LEN, str(opts.mimetype || 'image/jpeg')),
|
|
77
|
+
field(3, WIRE_LEN, str(opts.caption || '')),
|
|
78
|
+
field(4, WIRE_LEN, bytes(opts.fileSha256)),
|
|
79
|
+
field(5, WIRE_VARINT, varint(opts.fileLength)),
|
|
80
|
+
field(6, WIRE_VARINT, varint(opts.height || 0)),
|
|
81
|
+
field(7, WIRE_VARINT, varint(opts.width || 0)),
|
|
82
|
+
field(8, WIRE_LEN, bytes(opts.mediaKey)),
|
|
83
|
+
field(9, WIRE_LEN, bytes(opts.fileEncSha256)),
|
|
84
|
+
field(11, WIRE_LEN, str(opts.directPath)),
|
|
85
|
+
field(12, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
|
|
86
|
+
];
|
|
87
|
+
if (opts.jpegThumbnail) parts.push(field(16, WIRE_LEN, bytes(opts.jpegThumbnail)));
|
|
88
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
89
|
+
return Buffer.concat(parts);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function encodeVideoMessage(opts) {
|
|
93
|
+
const parts = [
|
|
94
|
+
field(1, WIRE_LEN, str(opts.url)),
|
|
95
|
+
field(2, WIRE_LEN, str(opts.mimetype || 'video/mp4')),
|
|
96
|
+
field(3, WIRE_LEN, bytes(opts.fileSha256)),
|
|
97
|
+
field(4, WIRE_VARINT, varint(opts.fileLength)),
|
|
98
|
+
field(5, WIRE_VARINT, varint(opts.seconds || 0)),
|
|
99
|
+
field(6, WIRE_LEN, bytes(opts.mediaKey)),
|
|
100
|
+
field(7, WIRE_LEN, str(opts.caption || '')),
|
|
101
|
+
field(9, WIRE_VARINT, varint(opts.height || 0)),
|
|
102
|
+
field(10, WIRE_VARINT, varint(opts.width || 0)),
|
|
103
|
+
field(11, WIRE_LEN, bytes(opts.fileEncSha256)),
|
|
104
|
+
field(12, WIRE_LEN, str(opts.directPath)),
|
|
105
|
+
field(13, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
|
|
106
|
+
];
|
|
107
|
+
if (opts.jpegThumbnail) parts.push(field(16, WIRE_LEN, bytes(opts.jpegThumbnail)));
|
|
108
|
+
if (opts.gifPlayback) parts.push(field(8, WIRE_VARINT, bool(true)));
|
|
109
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
110
|
+
return Buffer.concat(parts);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function encodeAudioMessage(opts) {
|
|
114
|
+
const parts = [
|
|
115
|
+
field(1, WIRE_LEN, str(opts.url)),
|
|
116
|
+
field(2, WIRE_LEN, str(opts.mimetype || 'audio/ogg; codecs=opus')),
|
|
117
|
+
field(3, WIRE_LEN, bytes(opts.fileSha256)),
|
|
118
|
+
field(4, WIRE_VARINT, varint(opts.fileLength)),
|
|
119
|
+
field(5, WIRE_VARINT, varint(opts.seconds || 0)),
|
|
120
|
+
field(6, WIRE_VARINT, bool(opts.ptt || false)),
|
|
121
|
+
field(7, WIRE_LEN, bytes(opts.mediaKey)),
|
|
122
|
+
field(8, WIRE_LEN, bytes(opts.fileEncSha256)),
|
|
123
|
+
field(9, WIRE_LEN, str(opts.directPath)),
|
|
124
|
+
field(10, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
|
|
125
|
+
];
|
|
126
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
127
|
+
return Buffer.concat(parts);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function encodeDocumentMessage(opts) {
|
|
131
|
+
const parts = [
|
|
132
|
+
field(1, WIRE_LEN, str(opts.url)),
|
|
133
|
+
field(2, WIRE_LEN, str(opts.mimetype || 'application/octet-stream')),
|
|
134
|
+
field(3, WIRE_LEN, str(opts.title || opts.fileName || 'file')),
|
|
135
|
+
field(4, WIRE_LEN, bytes(opts.fileSha256)),
|
|
136
|
+
field(5, WIRE_VARINT, varint(opts.fileLength)),
|
|
137
|
+
field(7, WIRE_LEN, bytes(opts.mediaKey)),
|
|
138
|
+
field(8, WIRE_LEN, str(opts.fileName || 'file')),
|
|
139
|
+
field(9, WIRE_LEN, bytes(opts.fileEncSha256)),
|
|
140
|
+
field(10, WIRE_LEN, str(opts.directPath)),
|
|
141
|
+
field(11, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
|
|
142
|
+
];
|
|
143
|
+
if (opts.jpegThumbnail) parts.push(field(16, WIRE_LEN, bytes(opts.jpegThumbnail)));
|
|
144
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
145
|
+
return Buffer.concat(parts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function encodeStickerMessage(opts) {
|
|
149
|
+
const parts = [
|
|
150
|
+
field(1, WIRE_LEN, str(opts.url)),
|
|
151
|
+
field(2, WIRE_LEN, bytes(opts.fileSha256)),
|
|
152
|
+
field(3, WIRE_LEN, bytes(opts.fileEncSha256)),
|
|
153
|
+
field(4, WIRE_LEN, bytes(opts.mediaKey)),
|
|
154
|
+
field(5, WIRE_LEN, str(opts.mimetype || 'image/webp')),
|
|
155
|
+
field(6, WIRE_VARINT, varint(opts.height || 512)),
|
|
156
|
+
field(7, WIRE_VARINT, varint(opts.width || 512)),
|
|
157
|
+
field(8, WIRE_LEN, str(opts.directPath)),
|
|
158
|
+
field(9, WIRE_VARINT, varint(opts.fileLength)),
|
|
159
|
+
field(10, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
|
|
160
|
+
];
|
|
161
|
+
if (opts.isAnimated) parts.push(field(13, WIRE_VARINT, bool(true)));
|
|
162
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
163
|
+
return Buffer.concat(parts);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function encodeReactionMessage(opts) {
|
|
167
|
+
const parts = [
|
|
168
|
+
field(1, WIRE_LEN, encodeMessageKey(opts.key)),
|
|
169
|
+
field(2, WIRE_LEN, str(opts.text || '')),
|
|
170
|
+
field(4, WIRE_VARINT, varintS(opts.senderTimestampMs || Date.now()))
|
|
171
|
+
];
|
|
172
|
+
return Buffer.concat(parts);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function encodeMessageKey(key) {
|
|
176
|
+
return Buffer.concat([
|
|
177
|
+
field(1, WIRE_LEN, str(key.remoteJid)),
|
|
178
|
+
field(2, WIRE_VARINT, bool(key.fromMe || false)),
|
|
179
|
+
field(3, WIRE_LEN, str(key.id))
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function encodeMessage(type, payload) {
|
|
184
|
+
switch (type) {
|
|
185
|
+
case 'text': return payload;
|
|
186
|
+
case 'image': return field(3, WIRE_LEN, payload);
|
|
187
|
+
case 'document': return field(7, WIRE_LEN, payload);
|
|
188
|
+
case 'audio': return field(8, WIRE_LEN, payload);
|
|
189
|
+
case 'video': return field(9, WIRE_LEN, payload);
|
|
190
|
+
case 'sticker': return field(26, WIRE_LEN, payload);
|
|
191
|
+
case 'reaction': return field(46, WIRE_LEN, payload);
|
|
192
|
+
default: return payload;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── DeviceSentMessage wrapper (field 31 of Message) ─────────────────────────
|
|
197
|
+
// Used when sending to own linked devices — wraps the original message so other
|
|
198
|
+
// devices know where the message was originally addressed (destinationJid).
|
|
199
|
+
// DeviceSentMessage { destinationJid=1, message=2, phash=3 }
|
|
200
|
+
|
|
201
|
+
function encodeDeviceSentMessage(destinationJid, messageBuf, phash) {
|
|
202
|
+
const inner = Buffer.concat([
|
|
203
|
+
field(1, WIRE_LEN, str(destinationJid)),
|
|
204
|
+
field(2, WIRE_LEN, messageBuf),
|
|
205
|
+
phash ? field(3, WIRE_LEN, str(phash)) : Buffer.alloc(0)
|
|
206
|
+
]);
|
|
207
|
+
return field(31, WIRE_LEN, inner);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
encodeText,
|
|
212
|
+
encodeImageMessage,
|
|
213
|
+
encodeVideoMessage,
|
|
214
|
+
encodeAudioMessage,
|
|
215
|
+
encodeDocumentMessage,
|
|
216
|
+
encodeStickerMessage,
|
|
217
|
+
encodeReactionMessage,
|
|
218
|
+
encodeMessageKey,
|
|
219
|
+
encodeMessage,
|
|
220
|
+
encodeDeviceSentMessage,
|
|
221
|
+
encodeContextInfo,
|
|
222
|
+
encodeVarint,
|
|
223
|
+
field,
|
|
224
|
+
varint,
|
|
225
|
+
str,
|
|
226
|
+
bytes,
|
|
227
|
+
WIRE_VARINT,
|
|
228
|
+
WIRE_LEN
|
|
229
|
+
};
|