libzapitu 1.0.0-alpha.22 → 1.0.0-alpha.24
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/lib/Socket/chats.js +23 -13
- package/lib/worker/proxy.js +184 -9
- package/package.json +1 -1
package/lib/Socket/chats.js
CHANGED
|
@@ -467,20 +467,30 @@ const makeChatsSocket = (config) => {
|
|
|
467
467
|
* type = "image for the high res picture"
|
|
468
468
|
*/
|
|
469
469
|
const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
|
|
470
|
-
var _a;
|
|
470
|
+
var _a, _b;
|
|
471
471
|
jid = (0, WABinary_1.jidNormalizedUser)(jid);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
472
|
+
try {
|
|
473
|
+
const result = await query({
|
|
474
|
+
tag: 'iq',
|
|
475
|
+
attrs: {
|
|
476
|
+
target: jid,
|
|
477
|
+
to: WABinary_1.S_WHATSAPP_NET,
|
|
478
|
+
type: 'get',
|
|
479
|
+
xmlns: 'w:profile:picture'
|
|
480
|
+
},
|
|
481
|
+
content: [{ tag: 'picture', attrs: { type, query: 'url' } }]
|
|
482
|
+
}, timeoutMs);
|
|
483
|
+
const child = (0, WABinary_1.getBinaryNodeChild)(result, 'picture');
|
|
484
|
+
return (_a = child === null || child === void 0 ? void 0 : child.attrs) === null || _a === void 0 ? void 0 : _a.url;
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
// WhatsApp returns 404/"item-not-found" when the contact has no profile picture.
|
|
488
|
+
// This is a normal situation — return undefined instead of throwing.
|
|
489
|
+
if ((err === null || err === void 0 ? void 0 : err.data) === 404 || ((_b = err === null || err === void 0 ? void 0 : err.message) === null || _b === void 0 ? void 0 : _b.includes('item-not-found'))) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
throw err;
|
|
493
|
+
}
|
|
484
494
|
};
|
|
485
495
|
const sendPresenceUpdate = async (type, toJid) => {
|
|
486
496
|
const me = authState.creds.me;
|
package/lib/worker/proxy.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.makeWASocket = void 0;
|
|
4
7
|
exports.getStats = getStats;
|
|
@@ -27,7 +30,143 @@ const worker_threads_1 = require("worker_threads");
|
|
|
27
30
|
const events_1 = require("events");
|
|
28
31
|
const os_1 = require("os");
|
|
29
32
|
const fs_1 = require("fs");
|
|
33
|
+
const fs_2 = require("fs");
|
|
30
34
|
const path_1 = require("path");
|
|
35
|
+
const stream_1 = require("stream");
|
|
36
|
+
const util_1 = require("util");
|
|
37
|
+
const os_2 = require("os");
|
|
38
|
+
const long_1 = __importDefault(require("long"));
|
|
39
|
+
const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Stream serialization for RPC boundary
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/** Maximum bytes to buffer in memory before falling back to a temp file. */
|
|
44
|
+
const STREAM_BUFFER_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
45
|
+
/**
|
|
46
|
+
* Read a Readable stream into a Buffer, enforcing a maximum size.
|
|
47
|
+
* If the stream exceeds `maxBytes`, it is destroyed and an error is thrown.
|
|
48
|
+
*/
|
|
49
|
+
async function streamToBuffer(stream, maxBytes) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let totalBytes = 0;
|
|
52
|
+
for await (const chunk of stream) {
|
|
53
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
54
|
+
totalBytes += buf.length;
|
|
55
|
+
if (totalBytes > maxBytes) {
|
|
56
|
+
stream.destroy();
|
|
57
|
+
throw new Error(`Stream exceeds maximum buffer size of ${maxBytes} bytes`);
|
|
58
|
+
}
|
|
59
|
+
chunks.push(buf);
|
|
60
|
+
}
|
|
61
|
+
return Buffer.concat(chunks);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write a Readable stream to a temporary file.
|
|
65
|
+
* Returns the absolute path to the temp file.
|
|
66
|
+
*/
|
|
67
|
+
async function streamToTempFile(stream) {
|
|
68
|
+
const tmpPath = (0, path_1.join)((0, os_2.tmpdir)(), `zapitu-proxy-upload-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
69
|
+
const writeStream = (0, fs_2.createWriteStream)(tmpPath);
|
|
70
|
+
try {
|
|
71
|
+
await pipelineAsync(stream, writeStream);
|
|
72
|
+
return tmpPath;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// Clean up partial file on error
|
|
76
|
+
try {
|
|
77
|
+
await fs_1.promises.unlink(tmpPath);
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
/* best-effort */
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Attempt to determine the byte size of a stream from its known properties.
|
|
87
|
+
* Returns `undefined` if the size cannot be determined without consuming.
|
|
88
|
+
*/
|
|
89
|
+
function tryGetStreamSize(stream) {
|
|
90
|
+
// fs.ReadStream exposes bytesRead + a path we could stat, but bytesRead
|
|
91
|
+
// is only populated after reading. Check for content-length on HTTP-like
|
|
92
|
+
// streams (e.g. axios responses).
|
|
93
|
+
const headers = stream === null || stream === void 0 ? void 0 : stream.headers;
|
|
94
|
+
if (headers === null || headers === void 0 ? void 0 : headers['content-length']) {
|
|
95
|
+
const len = parseInt(headers['content-length'], 10);
|
|
96
|
+
if (!isNaN(len))
|
|
97
|
+
return len;
|
|
98
|
+
}
|
|
99
|
+
// readableLength is a Node.js internal that gives buffered bytes, not total.
|
|
100
|
+
// Not useful here.
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert a Readable stream into a postMessage-safe representation.
|
|
105
|
+
*
|
|
106
|
+
* Strategy:
|
|
107
|
+
* 1. If the stream size is known and ≤ 50 MB → buffer in memory.
|
|
108
|
+
* 2. If the stream size is known and > 50 MB → write to temp file.
|
|
109
|
+
* 3. If the stream size is unknown → write to temp file (safe default).
|
|
110
|
+
*
|
|
111
|
+
* The caller is responsible for cleaning up temp files after the RPC call
|
|
112
|
+
* completes (success or failure).
|
|
113
|
+
*/
|
|
114
|
+
async function serializeStream(stream) {
|
|
115
|
+
const knownSize = tryGetStreamSize(stream);
|
|
116
|
+
if (knownSize !== undefined && knownSize <= STREAM_BUFFER_MAX_BYTES) {
|
|
117
|
+
const buffer = await streamToBuffer(stream, STREAM_BUFFER_MAX_BYTES);
|
|
118
|
+
return { buffer };
|
|
119
|
+
}
|
|
120
|
+
// Unknown size or known to be large → temp file
|
|
121
|
+
const tempFilePath = await streamToTempFile(stream);
|
|
122
|
+
return { tempFilePath };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recursively walk an argument tree and convert any `{ stream: Readable }`
|
|
126
|
+
* objects into postMessage-safe representations.
|
|
127
|
+
*
|
|
128
|
+
* After this function, the args are safe to pass to `worker.postMessage()`.
|
|
129
|
+
* Returns a cleanup function that should be called after the RPC completes
|
|
130
|
+
* to remove any temp files that were created.
|
|
131
|
+
*/
|
|
132
|
+
async function serializeStreamArgs(args) {
|
|
133
|
+
const tempFiles = [];
|
|
134
|
+
const walk = async (value) => {
|
|
135
|
+
if (value === null || value === undefined)
|
|
136
|
+
return value;
|
|
137
|
+
if (typeof value !== 'object')
|
|
138
|
+
return value;
|
|
139
|
+
// Detect { stream: Readable } — the WAMediaPayloadStream shape
|
|
140
|
+
if ('stream' in value && value.stream instanceof stream_1.Readable) {
|
|
141
|
+
const stream = value.stream;
|
|
142
|
+
const result = await serializeStream(stream);
|
|
143
|
+
if ('tempFilePath' in result) {
|
|
144
|
+
tempFiles.push(result.tempFilePath);
|
|
145
|
+
// Replace with { url: filePath } so the worker's getStream()
|
|
146
|
+
// treats it as a local file path (WAMediaPayloadURL shape).
|
|
147
|
+
return { url: result.tempFilePath };
|
|
148
|
+
}
|
|
149
|
+
// Buffer — pass directly; the worker's getStream() handles Buffer natively
|
|
150
|
+
return result.buffer;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return Promise.all(value.map(v => walk(v)));
|
|
154
|
+
}
|
|
155
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
const result = {};
|
|
159
|
+
for (const [key, val] of Object.entries(value)) {
|
|
160
|
+
result[key] = await walk(val);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
};
|
|
164
|
+
const processed = await Promise.all(args.map(a => walk(a)));
|
|
165
|
+
const cleanup = async () => {
|
|
166
|
+
await Promise.allSettled(tempFiles.map(p => fs_1.promises.unlink(p).catch(() => { })));
|
|
167
|
+
};
|
|
168
|
+
return { processed, cleanup };
|
|
169
|
+
}
|
|
31
170
|
// ---------------------------------------------------------------------------
|
|
32
171
|
// Types
|
|
33
172
|
// ---------------------------------------------------------------------------
|
|
@@ -86,11 +225,13 @@ function createNoopLogger() {
|
|
|
86
225
|
}
|
|
87
226
|
/**
|
|
88
227
|
* Recursively revive objects that were sanitized by the child's
|
|
89
|
-
* sanitizeForPostMessage (e.g. Error sentinels)
|
|
228
|
+
* sanitizeForPostMessage (e.g. Error sentinels) or decomposed by
|
|
229
|
+
* structured clone (e.g. Long → { low, high, unsigned }).
|
|
90
230
|
*
|
|
91
231
|
* IMPORTANT: structured clone already produces a perfect copy of the data.
|
|
92
232
|
* We only need to recursively look for __error__ sentinels and convert
|
|
93
|
-
* them back to Error instances
|
|
233
|
+
* them back to Error instances, and revive Long-like plain objects back
|
|
234
|
+
* to real Long instances. Everything else passes through unchanged.
|
|
94
235
|
*/
|
|
95
236
|
function revivePostMessage(value) {
|
|
96
237
|
if (value !== null && typeof value === 'object') {
|
|
@@ -100,19 +241,25 @@ function revivePostMessage(value) {
|
|
|
100
241
|
err.stack = value.stack;
|
|
101
242
|
return err;
|
|
102
243
|
}
|
|
244
|
+
// Revive Long-like objects that were decomposed by structured clone.
|
|
245
|
+
// protobufjs uses Long for 64-bit integers; after postMessage they
|
|
246
|
+
// become { low, high, unsigned } plain objects.
|
|
247
|
+
if (isLongLike(value)) {
|
|
248
|
+
return new long_1.default(value.low, value.high, value.unsigned);
|
|
249
|
+
}
|
|
103
250
|
if (Array.isArray(value)) {
|
|
104
|
-
// Arrays: recurse to revive any Error sentinels inside
|
|
105
|
-
let
|
|
251
|
+
// Arrays: recurse to revive any Error sentinels / Longs inside
|
|
252
|
+
let changed = false;
|
|
106
253
|
const result = value.map(v => {
|
|
107
254
|
const revived = revivePostMessage(v);
|
|
108
255
|
if (revived !== v)
|
|
109
|
-
|
|
256
|
+
changed = true;
|
|
110
257
|
return revived;
|
|
111
258
|
});
|
|
112
|
-
return
|
|
259
|
+
return changed ? result : value;
|
|
113
260
|
}
|
|
114
|
-
// Plain objects: only recurse if they might contain __error__ sentinels
|
|
115
|
-
//
|
|
261
|
+
// Plain objects: only recurse if they might contain __error__ sentinels
|
|
262
|
+
// or Long-like objects.
|
|
116
263
|
if (value.constructor === Object || value.constructor === undefined) {
|
|
117
264
|
let changed = false;
|
|
118
265
|
const result = {};
|
|
@@ -128,6 +275,23 @@ function revivePostMessage(value) {
|
|
|
128
275
|
}
|
|
129
276
|
return value;
|
|
130
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Check whether a value looks like a protobufjs Long that was decomposed
|
|
280
|
+
* by structured clone. Long instances have exactly { low, high, unsigned }
|
|
281
|
+
* after crossing postMessage.
|
|
282
|
+
*/
|
|
283
|
+
function isLongLike(value) {
|
|
284
|
+
if (value === null || typeof value !== 'object')
|
|
285
|
+
return false;
|
|
286
|
+
const keys = Object.keys(value);
|
|
287
|
+
return (keys.length === 3 &&
|
|
288
|
+
keys.includes('low') &&
|
|
289
|
+
keys.includes('high') &&
|
|
290
|
+
keys.includes('unsigned') &&
|
|
291
|
+
typeof value.low === 'number' &&
|
|
292
|
+
typeof value.high === 'number' &&
|
|
293
|
+
typeof value.unsigned === 'boolean');
|
|
294
|
+
}
|
|
131
295
|
// ---------------------------------------------------------------------------
|
|
132
296
|
// Pool management
|
|
133
297
|
// ---------------------------------------------------------------------------
|
|
@@ -255,7 +419,7 @@ function spawnWorker() {
|
|
|
255
419
|
}
|
|
256
420
|
else {
|
|
257
421
|
log === null || log === void 0 ? void 0 : log.trace({ rpcId: msg.id }, 'worker RPC result');
|
|
258
|
-
p.resolve(msg.result);
|
|
422
|
+
p.resolve(revivePostMessage(msg.result));
|
|
259
423
|
}
|
|
260
424
|
}
|
|
261
425
|
break;
|
|
@@ -635,6 +799,17 @@ const makeWASocket = (config) => {
|
|
|
635
799
|
cleanupSocket(slot, socketId, log);
|
|
636
800
|
};
|
|
637
801
|
}
|
|
802
|
+
if (prop === 'sendMessage') {
|
|
803
|
+
return async (...args) => {
|
|
804
|
+
const { processed, cleanup } = await serializeStreamArgs(args);
|
|
805
|
+
try {
|
|
806
|
+
return await rpcCall('sendMessage', processed);
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
await cleanup();
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
638
813
|
return (...args) => rpcCall(prop, args);
|
|
639
814
|
},
|
|
640
815
|
set(_target, prop, value) {
|