@oxidezap/baileyrs 0.0.35 → 0.1.1
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 +147 -24
- package/lib/Compatibility/legacy-store/namespaces.d.ts +20 -0
- package/lib/Compatibility/legacy-store/namespaces.js +27 -0
- package/lib/Compatibility/newsletter-results.d.ts +15 -0
- package/lib/Compatibility/newsletter-results.js +38 -0
- package/lib/Compatibility/proto-runtime.js +30 -20
- package/lib/Compatibility/websocket-client.d.ts +23 -2
- package/lib/Compatibility/websocket-client.js +47 -18
- package/lib/Socket/bridge-client-owner.d.ts +89 -0
- package/lib/Socket/bridge-client-owner.js +135 -0
- package/lib/Socket/business.d.ts +29 -0
- package/lib/Socket/business.js +104 -0
- package/lib/Socket/chat-actions.d.ts +20 -11
- package/lib/Socket/chat-actions.js +171 -83
- package/lib/Socket/events.d.ts +31 -0
- package/lib/Socket/events.js +144 -41
- package/lib/Socket/index.d.ts +113 -16
- package/lib/Socket/index.js +468 -157
- package/lib/Socket/internals.d.ts +88 -0
- package/lib/Socket/internals.js +145 -0
- package/lib/Socket/messages.d.ts +1 -12
- package/lib/Socket/messages.js +3 -20
- package/lib/Socket/newsletter.d.ts +61 -6
- package/lib/Socket/newsletter.js +125 -7
- package/lib/Socket/privacy.d.ts +25 -0
- package/lib/Socket/privacy.js +54 -0
- package/lib/Socket/server-queries.d.ts +38 -0
- package/lib/Socket/server-queries.js +121 -0
- package/lib/Socket/terminal-close-reporter.d.ts +79 -0
- package/lib/Socket/terminal-close-reporter.js +108 -0
- package/lib/Socket/terminal-close.d.ts +39 -0
- package/lib/Socket/terminal-close.js +51 -0
- package/lib/Socket/types.d.ts +6 -0
- package/lib/Types/Product.d.ts +9 -0
- package/lib/Utils/event-buffer.js +31 -0
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/link-preview.d.ts +60 -0
- package/lib/Utils/link-preview.js +357 -0
- package/lib/Utils/messages.d.ts +31 -7
- package/lib/Utils/messages.js +49 -17
- package/lib/Utils/wrap-legacy-store.d.ts +1 -0
- package/lib/Utils/wrap-legacy-store.js +1 -0
- package/package.json +4 -2
package/lib/Socket/index.js
CHANGED
|
@@ -16,20 +16,26 @@ import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
|
16
16
|
import { DisconnectReason } from '../Types/index.js';
|
|
17
17
|
import { Boom } from '../Utils/boom.js';
|
|
18
18
|
import { makeEventBuffer } from '../Utils/event-buffer.js';
|
|
19
|
-
import { _registerActiveBridgeClient, downloadMediaMessage } from '../Utils/messages.js';
|
|
19
|
+
import { _registerActiveBridgeClient, _unregisterActiveBridgeClient, downloadMediaMessage } from '../Utils/messages.js';
|
|
20
20
|
import { makeNativeCryptoProvider } from '../Utils/native-crypto-provider.js';
|
|
21
21
|
import { wrapLegacyStore } from '../Utils/wrap-legacy-store.js';
|
|
22
22
|
import { assertNodeErrorFree } from '../WABinary/generic-utils.js';
|
|
23
23
|
import { makeBlockingMethods } from './blocking.js';
|
|
24
|
+
import { makeBusinessMethods } from './business.js';
|
|
24
25
|
import { makeChatActionMethods } from './chat-actions.js';
|
|
25
26
|
import { makeContactMethods } from './contacts.js';
|
|
26
27
|
import { makeCommunityMethods } from './communities.js';
|
|
28
|
+
import { makeBridgeClientOwner } from './bridge-client-owner.js';
|
|
29
|
+
import { makeTerminalCloseReporter } from './terminal-close-reporter.js';
|
|
27
30
|
import { makeEventHandlers } from './events.js';
|
|
28
31
|
import { makeGroupMethods } from './groups.js';
|
|
32
|
+
import { makeInternalMethods, makeUnexpectedErrorReporter } from './internals.js';
|
|
29
33
|
import { makeMessageMethods } from './messages.js';
|
|
30
34
|
import { makeNewsletterMethods } from './newsletter.js';
|
|
31
35
|
import { makePreKeyMethods } from './prekeys.js';
|
|
32
36
|
import { makePresenceMethods } from './presence.js';
|
|
37
|
+
import { makePrivacyMethods } from './privacy.js';
|
|
38
|
+
import { makeServerQueryMethods } from './server-queries.js';
|
|
33
39
|
import { makeProfileMethods } from './profile.js';
|
|
34
40
|
import { mapReachoutTimelock } from './reachout.js';
|
|
35
41
|
import { makeHttpClient, makeTransport } from './transport.js';
|
|
@@ -72,10 +78,160 @@ const makeWASocket = (config) => {
|
|
|
72
78
|
// this first so `ev.on('creds.update', saveCreds)` persists the merged state
|
|
73
79
|
// rather than the pre-pair placeholder.
|
|
74
80
|
ev.on('creds.update', update => Object.assign(auth.creds, update));
|
|
75
|
-
let client;
|
|
76
|
-
let readyClient;
|
|
77
81
|
let user;
|
|
78
|
-
|
|
82
|
+
/** True once `init()` has finished wiring the client and started its read loop. */
|
|
83
|
+
let initialized = false;
|
|
84
|
+
/** True while the socket's end handlers run — see `end`. */
|
|
85
|
+
let runningEndHandlers = false;
|
|
86
|
+
/**
|
|
87
|
+
* Consumer teardown hooks, plus the socket's own. Declared here rather than
|
|
88
|
+
* beside their registrations so the owner below can close over the list
|
|
89
|
+
* before anything fills it.
|
|
90
|
+
*/
|
|
91
|
+
const socketEndHandlers = [];
|
|
92
|
+
/**
|
|
93
|
+
* Drain both auth stores, returning the FIRST failure rather than throwing
|
|
94
|
+
* so the caller can finish the rest of its work and still report it.
|
|
95
|
+
*
|
|
96
|
+
* Called through their owners on purpose: collecting the two methods into
|
|
97
|
+
* an array and invoking them bare drops the receiver, so a consumer store
|
|
98
|
+
* whose `flush()` touches `this` throws on `undefined` and the teardown
|
|
99
|
+
* publishes a close with the auth writes unpersisted.
|
|
100
|
+
*/
|
|
101
|
+
const flushStores = async () => {
|
|
102
|
+
let firstError;
|
|
103
|
+
try {
|
|
104
|
+
await auth.store?.flush?.();
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
firstError ?? (firstError = e);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await autoWrappedStore?.flush?.();
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
firstError ?? (firstError = e);
|
|
114
|
+
}
|
|
115
|
+
return firstError;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Single home for the bridge client's lifetime. `ws` below reads the current
|
|
119
|
+
* client from it, and this teardown closes `ws` — the cycle is fine because
|
|
120
|
+
* both directions only run once the socket is live.
|
|
121
|
+
*/
|
|
122
|
+
const owner = makeBridgeClientOwner({
|
|
123
|
+
logger,
|
|
124
|
+
/**
|
|
125
|
+
* Everything the socket owns beyond the client itself. Runs once, with
|
|
126
|
+
* the client still usable, whether or not one was ever adopted — a
|
|
127
|
+
* teardown that landed mid-init still has a transport to close and a
|
|
128
|
+
* store to drain.
|
|
129
|
+
*/
|
|
130
|
+
teardown: async (client, error) => {
|
|
131
|
+
try {
|
|
132
|
+
await ws.close();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// The transport refused to close cleanly. Go straight at the
|
|
136
|
+
// client so the disconnect still happens *before* the barrier
|
|
137
|
+
// and flush below: `release` retries it, but that runs after the
|
|
138
|
+
// flush, and the closing-session ratchet writes a disconnect
|
|
139
|
+
// enqueues would then have nothing left to persist them.
|
|
140
|
+
try {
|
|
141
|
+
await client?.disconnect();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* ignore */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (client) {
|
|
148
|
+
// Barrier: bridge cleanup paths fired during `disconnect()` may
|
|
149
|
+
// emit `set()` calls that are still queued as microtasks /
|
|
150
|
+
// `setImmediate` callbacks at this point. Two yields to the
|
|
151
|
+
// event loop drain (1) the microtask queue and (2) the next
|
|
152
|
+
// macrotask tick where wasm-bindgen async callbacks land.
|
|
153
|
+
// Without this barrier the flushes below run before the bridge
|
|
154
|
+
// has finished writing — a race that loses the last few sets
|
|
155
|
+
// (typically the closing-session ratchet step).
|
|
156
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
157
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
158
|
+
}
|
|
159
|
+
const firstFlushError = await flushStores();
|
|
160
|
+
// End handlers run before the flush error is rethrown: they are the
|
|
161
|
+
// consumer's teardown hook, and a corrupt-on-shutdown auth store is
|
|
162
|
+
// exactly when they most need to run.
|
|
163
|
+
//
|
|
164
|
+
// The flag makes a re-entrant `end()` from inside one of them a
|
|
165
|
+
// no-op instead of a deadlock: shared cleanup used both directly and
|
|
166
|
+
// as an end hook would otherwise be handed the very promise that is
|
|
167
|
+
// waiting for it to return, and nothing would ever settle — no
|
|
168
|
+
// release, and no terminal close until the watchdog.
|
|
169
|
+
runningEndHandlers = true;
|
|
170
|
+
try {
|
|
171
|
+
for (const handler of socketEndHandlers) {
|
|
172
|
+
try {
|
|
173
|
+
await handler(error);
|
|
174
|
+
}
|
|
175
|
+
catch (handlerError) {
|
|
176
|
+
logger.error({ err: handlerError }, 'error in socket end handler');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
runningEndHandlers = false;
|
|
182
|
+
}
|
|
183
|
+
if (firstFlushError)
|
|
184
|
+
throw firstFlushError;
|
|
185
|
+
},
|
|
186
|
+
release: async (client) => {
|
|
187
|
+
// `disconnect()` before `free()` is defence in depth, not a fix for a
|
|
188
|
+
// reproduced bug on this path.
|
|
189
|
+
//
|
|
190
|
+
// The hazard is real and reproducible at the bridge: freeing a client
|
|
191
|
+
// with any call still pending corrupts the wasm heap — dlmalloc trips
|
|
192
|
+
// `assertion failed: psize <= size + max_overhead` and the process
|
|
193
|
+
// dies on `RuntimeError: unreachable`, from a microtask no try/catch
|
|
194
|
+
// here can reach, since `free()` itself returns normally.
|
|
195
|
+
// `logout()`, `disconnect()` and a plain `fetchBlocklist()` all
|
|
196
|
+
// reproduce it — see `__tests__/bridge-free-safety.test.ts`.
|
|
197
|
+
//
|
|
198
|
+
// What keeps teardown off that path is the `ws.close()` above, which
|
|
199
|
+
// is itself a `client.disconnect()` (`Compatibility/websocket-client.ts`).
|
|
200
|
+
// This is the belt to that braces, and the gap it closes is
|
|
201
|
+
// `WebSocketClient.close()`'s early return when `closing`/`closed` is
|
|
202
|
+
// already set: that path does NOT await the disconnect it skipped, so
|
|
203
|
+
// `void sock.ws.close(); await sock.end()` could otherwise reach
|
|
204
|
+
// `free()` with the first disconnect still running.
|
|
205
|
+
let disconnected = true;
|
|
206
|
+
try {
|
|
207
|
+
await client.disconnect();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
disconnected = false;
|
|
211
|
+
}
|
|
212
|
+
// Teardown already flushed, but only after its own disconnect
|
|
213
|
+
// attempts. If those all failed and this one succeeded, the
|
|
214
|
+
// closing-session ratchet writes it enqueues arrived after that
|
|
215
|
+
// flush — with nothing left to persist them. Cheap enough to just
|
|
216
|
+
// drain again.
|
|
217
|
+
if (disconnected) {
|
|
218
|
+
const lateFlushError = await flushStores();
|
|
219
|
+
if (lateFlushError)
|
|
220
|
+
logger.error({ err: lateFlushError }, 'failed to flush after the final disconnect');
|
|
221
|
+
}
|
|
222
|
+
// Unregister before freeing: `free()` is swallowed, so ordering it
|
|
223
|
+
// last would leave the module-level pointer aimed at a client that is
|
|
224
|
+
// already gone if anything between them threw.
|
|
225
|
+
_unregisterActiveBridgeClient(client);
|
|
226
|
+
try {
|
|
227
|
+
client.free();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* ignore */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
const ws = new WebSocketClient(fullConfig.waWebSocketUrl, fullConfig, () => owner.peek());
|
|
79
235
|
let tagEpoch = 0;
|
|
80
236
|
// Per-socket random prefix avoids collisions between sockets created
|
|
81
237
|
// in the same millisecond. Date.now()-based prefixes (the previous
|
|
@@ -91,11 +247,24 @@ const makeWASocket = (config) => {
|
|
|
91
247
|
// `end()` to drain the debounced `saveCreds` timer — `auth.store?.flush?.()`
|
|
92
248
|
// covers the explicit-store path but not this one.
|
|
93
249
|
let autoWrappedStore;
|
|
250
|
+
// Mirrors the engine's `enable_auto_reconnect`, which defaults to on. Only
|
|
251
|
+
// `sock.setAutoReconnect()` moves it, and the dispatcher reads it to tell a
|
|
252
|
+
// transient drop from a terminal one.
|
|
253
|
+
let autoReconnectEnabled = true;
|
|
254
|
+
/** Owns reporting the terminal close: once, after teardown, never not at all. */
|
|
255
|
+
const terminalClose = makeTerminalCloseReporter({ logger });
|
|
256
|
+
/**
|
|
257
|
+
* Held in a reporter rather than captured, because `sock.onUnexpectedError`
|
|
258
|
+
* is an assignable property: a consumer that replaces it has to be the one
|
|
259
|
+
* the socket's own failure paths reach afterwards.
|
|
260
|
+
*/
|
|
261
|
+
const unexpectedErrors = makeUnexpectedErrorReporter(logger);
|
|
94
262
|
const ctx = {
|
|
95
263
|
ev,
|
|
96
264
|
logger,
|
|
97
265
|
fullConfig,
|
|
98
266
|
ws,
|
|
267
|
+
reportUnexpectedError: unexpectedErrors.report,
|
|
99
268
|
getUser: () => user,
|
|
100
269
|
getMe: () => {
|
|
101
270
|
const me = auth.creds.me;
|
|
@@ -108,21 +277,58 @@ const makeWASocket = (config) => {
|
|
|
108
277
|
user = u;
|
|
109
278
|
},
|
|
110
279
|
getClient: () => {
|
|
111
|
-
|
|
112
|
-
|
|
280
|
+
// `peek()` keeps returning the client through `closing` so teardown
|
|
281
|
+
// can still close the transport with it — but that is teardown's
|
|
282
|
+
// client, not everyone's. Handing it to an ordinary call racing
|
|
283
|
+
// shutdown, or made from an end handler, starts a bridge operation
|
|
284
|
+
// while the client is being disconnected and freed, which is the
|
|
285
|
+
// heap-corruption hazard the whole teardown ordering exists to
|
|
286
|
+
// avoid. Refuse from the moment `close()` is called.
|
|
287
|
+
if (owner.isClosing()) {
|
|
288
|
+
return Promise.reject(new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }));
|
|
289
|
+
}
|
|
290
|
+
// Otherwise gated on `initialized`, not merely on the client
|
|
291
|
+
// existing. `adopt()` publishes it several awaits before
|
|
292
|
+
// `setDeviceProps`, the account lookups and `run()`, so keying off
|
|
293
|
+
// `peek()` alone would hand ordinary calls like `sendMessage()` a
|
|
294
|
+
// half-built client whose read loop has not started — and skip the
|
|
295
|
+
// `initError` check when startup later fails.
|
|
296
|
+
if (initialized) {
|
|
297
|
+
const ready = owner.peek();
|
|
298
|
+
if (ready)
|
|
299
|
+
return Promise.resolve(ready);
|
|
300
|
+
}
|
|
113
301
|
return initPromise.then(() => {
|
|
302
|
+
// Rechecked after the await: a close landing while startup was
|
|
303
|
+
// still running would otherwise be handed the client anyway.
|
|
304
|
+
//
|
|
305
|
+
// The window between handing a client back and the call reaching
|
|
306
|
+
// wasm cannot be closed here — that needs in-flight call
|
|
307
|
+
// tracking. What covers it is `release`, which awaits
|
|
308
|
+
// `client.disconnect()` before `free()`; the corruption comes
|
|
309
|
+
// from freeing with a call pending, and the disconnect drains
|
|
310
|
+
// those first (`__tests__/bridge-free-safety.test.ts`).
|
|
311
|
+
if (owner.isClosing()) {
|
|
312
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
313
|
+
}
|
|
114
314
|
if (initError) {
|
|
115
315
|
throw new Boom('Bridge client failed to initialize: ' + initError.message, { statusCode: 500 });
|
|
116
316
|
}
|
|
117
|
-
|
|
317
|
+
const built = owner.peek();
|
|
318
|
+
if (!built)
|
|
118
319
|
throw new Boom('Client not initialized', { statusCode: 500 });
|
|
119
|
-
return
|
|
320
|
+
return built;
|
|
120
321
|
});
|
|
121
322
|
},
|
|
122
323
|
getClientSync: () => {
|
|
123
|
-
|
|
324
|
+
// Same rule as `getClient` — see there.
|
|
325
|
+
if (owner.isClosing()) {
|
|
326
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
327
|
+
}
|
|
328
|
+
const built = owner.peek();
|
|
329
|
+
if (!built)
|
|
124
330
|
throw new Boom('Client not initialized', { statusCode: 500 });
|
|
125
|
-
return
|
|
331
|
+
return built;
|
|
126
332
|
}
|
|
127
333
|
};
|
|
128
334
|
// The native repository delegates Signal state directly to the core and does
|
|
@@ -140,6 +346,7 @@ const makeWASocket = (config) => {
|
|
|
140
346
|
const appStatePatchMutex = makeMutex();
|
|
141
347
|
const notificationMutex = makeMutex();
|
|
142
348
|
const activeCallContexts = new Map();
|
|
349
|
+
socketEndHandlers.push(() => activeCallContexts.clear());
|
|
143
350
|
const groupMethods = makeGroupMethods(ctx);
|
|
144
351
|
const communityMethods = makeCommunityMethods(ctx, groupMethods);
|
|
145
352
|
const refreshParticipating = makeParticipatingRefreshHandler(ctx, {
|
|
@@ -149,7 +356,8 @@ const makeWASocket = (config) => {
|
|
|
149
356
|
const eventHandlers = makeEventHandlers(ctx, {
|
|
150
357
|
onPairSuccess: data => {
|
|
151
358
|
pairedAccount = data;
|
|
152
|
-
|
|
359
|
+
owner
|
|
360
|
+
.peek()
|
|
153
361
|
?.getAccount?.()
|
|
154
362
|
.then((acc) => {
|
|
155
363
|
cachedAccount = acc ?? undefined;
|
|
@@ -165,7 +373,34 @@ const makeWASocket = (config) => {
|
|
|
165
373
|
activeCallContexts.set(callId, { peer: event.from, callCreator });
|
|
166
374
|
}
|
|
167
375
|
},
|
|
168
|
-
onDirtyState: event => refreshParticipating(event.dirtyType)
|
|
376
|
+
onDirtyState: event => refreshParticipating(event.dirtyType),
|
|
377
|
+
/**
|
|
378
|
+
* The engine has stopped reconnecting, so this client is dead weight
|
|
379
|
+
* that only `free()` reclaims — `run()` returns `void`, so its loop
|
|
380
|
+
* exiting is otherwise invisible from here.
|
|
381
|
+
*
|
|
382
|
+
* Tearing down and reporting are both handed to the reporter: the close
|
|
383
|
+
* has to reach the consumer exactly once and only after this socket has
|
|
384
|
+
* released what it owns, or a replacement built in response overlaps it
|
|
385
|
+
* on the same auth folder.
|
|
386
|
+
*/
|
|
387
|
+
onTerminalClose: (error, publish) => {
|
|
388
|
+
// `owner.close()`, not `end()`. `end()` short-circuits when called
|
|
389
|
+
// from inside an end handler — it has to, or the handler awaits the
|
|
390
|
+
// teardown waiting for it — and a terminal event raised from one of
|
|
391
|
+
// those would then publish against an already-resolved promise,
|
|
392
|
+
// letting a close listener build a replacement while the old client
|
|
393
|
+
// is still owned. This waits for the real teardown, and cannot
|
|
394
|
+
// deadlock because `reportAfter` runs it detached; nothing in the
|
|
395
|
+
// teardown is waiting on this.
|
|
396
|
+
terminalClose.reportAfter(() => owner.close(error).finally(() => initPromise), publish);
|
|
397
|
+
},
|
|
398
|
+
isAutoReconnectEnabled: () => autoReconnectEnabled,
|
|
399
|
+
// Timers the dispatcher armed outlive the events that armed them, and
|
|
400
|
+
// only the terminal-close path clears them. Ending the socket any other
|
|
401
|
+
// way — `sock.end()`, an `await using` scope exiting — has to as well,
|
|
402
|
+
// or one fires from a socket whose client is already freed.
|
|
403
|
+
onCleanup: cleanup => socketEndHandlers.push(cleanup)
|
|
169
404
|
});
|
|
170
405
|
const init = async () => {
|
|
171
406
|
if (!wasmInitialized) {
|
|
@@ -211,25 +446,50 @@ const makeWASocket = (config) => {
|
|
|
211
446
|
}
|
|
212
447
|
if (useNativeMemory)
|
|
213
448
|
logger.debug('auth: using socket-local native memory backend');
|
|
214
|
-
|
|
449
|
+
const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null);
|
|
450
|
+
// `end()` can land while the client is still being built — a `sock.end()`
|
|
451
|
+
// or `await using` right after `makeWASocket()` does exactly that. When
|
|
452
|
+
// it has, `adopt` frees this client and tells us to stop: nothing else
|
|
453
|
+
// would ever own it, and `run()` below would reconnect it forever
|
|
454
|
+
// against a socket the caller already disposed.
|
|
455
|
+
// `adopt` starts releasing the refused client; joining it here keeps
|
|
456
|
+
// that work inside `initPromise`, which `Symbol.asyncDispose` awaits.
|
|
457
|
+
if (!owner.adopt(created))
|
|
458
|
+
return owner.settled();
|
|
459
|
+
// Fallback for standalone helpers like `downloadContentFromMessage`
|
|
460
|
+
// that carry no socket reference.
|
|
461
|
+
_registerActiveBridgeClient(created, logger);
|
|
462
|
+
// Replay a preference set before the client existed. `setAutoReconnect`
|
|
463
|
+
// forwards through `client?.`, so `makeWASocket(cfg).setAutoReconnect(false)`
|
|
464
|
+
// — the idiomatic first line — used to move only the JS mirror and leave
|
|
465
|
+
// the engine retrying.
|
|
466
|
+
if (!autoReconnectEnabled)
|
|
467
|
+
created.setAutoReconnect(false);
|
|
468
|
+
// Everything below talks to `created`, which stays valid for the whole
|
|
469
|
+
// body, and re-checks `isClosing()` between awaits: once teardown has
|
|
470
|
+
// started it owns this client, and issuing more bridge calls against it
|
|
471
|
+
// races the release.
|
|
215
472
|
if (fullConfig.pushName) {
|
|
216
|
-
await
|
|
473
|
+
await created.setInitialPushName(fullConfig.pushName);
|
|
217
474
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
_registerActiveBridgeClient(client, logger);
|
|
475
|
+
if (owner.isClosing())
|
|
476
|
+
return;
|
|
221
477
|
const [osName, browserName] = fullConfig.browser;
|
|
222
478
|
const deviceOs = browserName === 'Android' ? 'Android' : osName;
|
|
223
|
-
await
|
|
479
|
+
await created.setDeviceProps({
|
|
224
480
|
os: deviceOs,
|
|
225
481
|
platformType: browserToPlatformType(browserName),
|
|
226
482
|
...fullConfig.deviceProps
|
|
227
483
|
});
|
|
484
|
+
if (owner.isClosing())
|
|
485
|
+
return;
|
|
228
486
|
const [jid, lid, account] = await Promise.all([
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
487
|
+
created.getJid(),
|
|
488
|
+
created.getLid(),
|
|
489
|
+
created.getAccount().catch(() => undefined)
|
|
232
490
|
]);
|
|
491
|
+
if (owner.isClosing())
|
|
492
|
+
return;
|
|
233
493
|
if (jid) {
|
|
234
494
|
user = { id: jid, lid: lid ?? undefined };
|
|
235
495
|
}
|
|
@@ -240,129 +500,145 @@ const makeWASocket = (config) => {
|
|
|
240
500
|
// `UserAgent.platform = ANDROID` (no `web_info`), mirroring upstream
|
|
241
501
|
// Baileys PR #2201. Required for the server to deliver view_once payloads.
|
|
242
502
|
if (browserName === 'Android') {
|
|
243
|
-
await
|
|
503
|
+
await created.setClientProfile({ preset: 'android', osVersion: osName });
|
|
504
|
+
if (owner.isClosing())
|
|
505
|
+
return;
|
|
244
506
|
}
|
|
245
507
|
if (isRawNodeForwardingEnabled(ws)) {
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
// `run()` is fire-and-forget by design (the bridge runs the read
|
|
249
|
-
// loop until disconnect/free) but typed `Promise<void>`. A late
|
|
250
|
-
// rejection (lost connection during cleanup, etc) without a
|
|
251
|
-
// `.catch` would escape to `process.on('unhandledRejection')`.
|
|
252
|
-
// Funnel into the connection.update channel so consumers' regular
|
|
253
|
-
// reconnect/diagnostic plumbing handles it like any other close.
|
|
254
|
-
const runPromise = client.run();
|
|
255
|
-
if (runPromise && typeof runPromise.catch === 'function') {
|
|
256
|
-
runPromise.catch(err => {
|
|
257
|
-
logger.error({ err }, 'bridge client.run() rejected');
|
|
258
|
-
ev.emit('connection.update', {
|
|
259
|
-
connection: 'close',
|
|
260
|
-
lastDisconnect: {
|
|
261
|
-
// restartRequired (515) signals "Rust read loop crashed
|
|
262
|
-
// unrecoverably, restart the sock". Previously mapped to
|
|
263
|
-
// 500, which collided with the server-side <stream:error
|
|
264
|
-
// code="500"> path and made it impossible for consumers
|
|
265
|
-
// to tell the two apart.
|
|
266
|
-
error: err instanceof Error ? err : new Boom(String(err), { statusCode: DisconnectReason.restartRequired }),
|
|
267
|
-
date: new Date()
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
508
|
+
created.setRawNodeForwarding(true);
|
|
271
509
|
}
|
|
510
|
+
// Same race as above: teardown already owns and releases this client, so
|
|
511
|
+
// starting the read loop now would run against a handle about to go.
|
|
512
|
+
if (owner.isClosing())
|
|
513
|
+
return;
|
|
514
|
+
// `run()` spawns the connect/handshake/read/reconnect loop as a
|
|
515
|
+
// background task and returns `void` — it deliberately is not `async`,
|
|
516
|
+
// so that it does not hold a wasm-bindgen borrow on `self` that would
|
|
517
|
+
// block `disconnect()`.
|
|
518
|
+
//
|
|
519
|
+
// Consequence: the loop's exit is not observable from here. The engine
|
|
520
|
+
// clears `enable_auto_reconnect` and breaks out on every terminal
|
|
521
|
+
// disconnect (conflict/401/409/516, and any `<failure>` whose reason is
|
|
522
|
+
// not 500/503), and when it does, the `WasmWhatsAppClient` is dead
|
|
523
|
+
// weight that only `sock.end()` can free — nothing else can, because
|
|
524
|
+
// the bridge holds the JS event callbacks as wasm-bindgen externrefs,
|
|
525
|
+
// those close over `ctx`, and `ctx` closes over `client`, so the cycle
|
|
526
|
+
// crosses the JS/wasm boundary and no `FinalizationRegistry` fires.
|
|
527
|
+
// Freeing that automatically needs the bridge to expose loop completion
|
|
528
|
+
// (a terminal callback or an awaitable handle); until it does, the
|
|
529
|
+
// consumer has to call `sock.end()` on a terminal close.
|
|
530
|
+
created.run();
|
|
531
|
+
initialized = true;
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* Joins startup too, not just the teardown.
|
|
535
|
+
*
|
|
536
|
+
* `owner.close()` covers the client it can see. A close landing while
|
|
537
|
+
* `createWhatsAppClient()` is still pending sees none — the client arrives
|
|
538
|
+
* afterwards, `adopt()` refuses it, and the release runs detached. Without
|
|
539
|
+
* waiting for `init()` to finish, `await sock.end()` therefore returns while
|
|
540
|
+
* that client is still disconnecting and being freed, and the replacement
|
|
541
|
+
* socket the consumer builds next overlaps it on the same auth folder.
|
|
542
|
+
*
|
|
543
|
+
* `initPromise` is declared below and swallows its own failures, so this
|
|
544
|
+
* neither hits its TDZ (nothing can call `end` during the synchronous
|
|
545
|
+
* construction below) nor masks the teardown error.
|
|
546
|
+
*/
|
|
547
|
+
const end = (error) => {
|
|
548
|
+
// Called from inside an end handler, the teardown is already running and
|
|
549
|
+
// is waiting for that handler to return. Handing back its promise would
|
|
550
|
+
// have the handler await itself.
|
|
551
|
+
if (runningEndHandlers)
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
return owner.close(error).finally(() => initPromise);
|
|
272
554
|
};
|
|
273
555
|
let initError;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
556
|
+
// Started only once `end` exists. `init()`'s synchronous prefix reaches
|
|
557
|
+
// `await createWhatsAppClient(...)` before the bridge can dispatch anything,
|
|
558
|
+
// so today nothing can call `onTerminalClose` — and therefore `end` — that
|
|
559
|
+
// early. But that is an argument about the current shape of `init()`, not a
|
|
560
|
+
// rule the code enforces: dispatch a terminal close any sooner and line 401
|
|
561
|
+
// becomes a `ReferenceError` inside a bridge callback, where no `try/catch`
|
|
562
|
+
// of ours can reach it. Ordering it here makes the dependency structural.
|
|
563
|
+
const initPromise = init().catch(err => {
|
|
280
564
|
initError = err instanceof Error ? err : new Error(String(err));
|
|
281
565
|
logger.error({ err }, 'failed to initialize bridge client');
|
|
566
|
+
// A client adopted before the failure outlives a read loop that never
|
|
567
|
+
// started: `getClient()` correctly rejects, but the standalone
|
|
568
|
+
// helpers bypass it and would keep reaching the half-built client.
|
|
569
|
+
return owner.discard();
|
|
282
570
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
ended = true;
|
|
293
|
-
const c = client;
|
|
294
|
-
if (c) {
|
|
295
|
-
try {
|
|
296
|
-
await ws.close();
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
/* ignore */
|
|
300
|
-
}
|
|
301
|
-
client = undefined;
|
|
302
|
-
readyClient = undefined;
|
|
303
|
-
// Barrier: bridge cleanup paths fired during `disconnect()` may
|
|
304
|
-
// emit `set()` calls that are still queued as microtasks /
|
|
305
|
-
// `setImmediate` callbacks at this point. Two yields to the
|
|
306
|
-
// event loop drain (1) microtask queue and (2) the next
|
|
307
|
-
// macrotask tick where wasm-bindgen async callbacks land.
|
|
308
|
-
// Without this barrier, the flushes below run before the bridge
|
|
309
|
-
// has finished writing — a race that loses the last few sets
|
|
310
|
-
// (typically the closing-session ratchet step).
|
|
311
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
312
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
313
|
-
// Capture the FIRST flush failure so a corrupt-on-shutdown auth
|
|
314
|
-
// state surfaces to the caller. Always finish the rest of
|
|
315
|
-
// cleanup; rethrow at the end so c.free() still runs.
|
|
316
|
-
let firstFlushError;
|
|
317
|
-
try {
|
|
318
|
-
await auth.store?.flush?.();
|
|
319
|
-
}
|
|
320
|
-
catch (e) {
|
|
321
|
-
firstFlushError ?? (firstFlushError = e);
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
await autoWrappedStore?.flush?.();
|
|
325
|
-
}
|
|
326
|
-
catch (e) {
|
|
327
|
-
firstFlushError ?? (firstFlushError = e);
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
c.free();
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
/* ignore */
|
|
334
|
-
}
|
|
335
|
-
if (firstFlushError)
|
|
336
|
-
throw firstFlushError;
|
|
337
|
-
}
|
|
338
|
-
for (const handler of socketEndHandlers) {
|
|
339
|
-
try {
|
|
340
|
-
await handler(error);
|
|
341
|
-
}
|
|
342
|
-
catch (handlerError) {
|
|
343
|
-
logger.error({ err: handlerError }, 'error in socket end handler');
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
};
|
|
571
|
+
/**
|
|
572
|
+
* Idempotent shutdown that later callers can actually await — the socket
|
|
573
|
+
* now ends *itself* on a terminal disconnect, so a consumer's
|
|
574
|
+
* `await sock.end()` is usually the second call. Resolving it early while
|
|
575
|
+
* the first was still flushing turned
|
|
576
|
+
* `close` → `await sock.end()` → `makeWASocket()` into two writers on one
|
|
577
|
+
* auth folder.
|
|
578
|
+
*/
|
|
347
579
|
const logout = async (msg) => {
|
|
348
580
|
user = undefined;
|
|
349
|
-
|
|
581
|
+
const logoutError = new Boom(msg || 'Logged out', { statusCode: DisconnectReason.loggedOut });
|
|
582
|
+
// `Client::logout()` dispatches `LoggedOut` itself, which the dispatcher
|
|
583
|
+
// turns into the terminal close. Reporting our own on top of that gave
|
|
584
|
+
// consumers two `close` events for one logout, and upstream guarantees
|
|
585
|
+
// at most one — so watch for the dispatcher's instead of assuming
|
|
586
|
+
// either way. Counting rather than flagging, because a terminal close
|
|
587
|
+
// may already have happened earlier in this socket's life.
|
|
588
|
+
// `Client::logout()` dispatches `LoggedOut` itself, which the dispatcher
|
|
589
|
+
// turns into the terminal close. Reporting our own on top of that gave
|
|
590
|
+
// consumers two closes for one logout, and upstream guarantees at most
|
|
591
|
+
// one — so watch for the dispatcher's instead of assuming either way.
|
|
592
|
+
const reportedBefore = terminalClose.hasReported();
|
|
593
|
+
const live = owner.peek();
|
|
594
|
+
if (live) {
|
|
350
595
|
try {
|
|
351
|
-
await
|
|
596
|
+
await live.logout();
|
|
352
597
|
}
|
|
353
598
|
catch {
|
|
354
599
|
/* ignore */
|
|
355
600
|
}
|
|
356
601
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
602
|
+
// Nothing announced it: no client at all, or `logout()` threw before the
|
|
603
|
+
// bridge got to it. Upstream always reports exactly one close for a
|
|
604
|
+
// logout, so emitting none would be worse than emitting one too many.
|
|
605
|
+
// Keyed on a close having been *reported*, not on the socket closing —
|
|
606
|
+
// a plain `end()` reports nothing, so keying off that would let a logout
|
|
607
|
+
// racing one finish with no close at all.
|
|
608
|
+
const mayNeedFallback = !reportedBefore && !terminalClose.hasReported();
|
|
609
|
+
try {
|
|
610
|
+
await end(logoutError);
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
// After the teardown, like the dispatcher's own close: doing it
|
|
614
|
+
// before would hand a logged-out handler a socket still flushing the
|
|
615
|
+
// auth folder it is about to delete. In a `finally` because `end()`
|
|
616
|
+
// rethrows the first flush failure and the owner releases the client
|
|
617
|
+
// regardless — on that path listeners would otherwise see no
|
|
618
|
+
// terminal close at all for a socket that has definitely ended.
|
|
619
|
+
// Rechecked here, not just before the await: a dispatcher close
|
|
620
|
+
// arriving while `end()` ran would otherwise be followed by this
|
|
621
|
+
// stale decision, giving consumers two closes for one logout.
|
|
622
|
+
if (mayNeedFallback && !terminalClose.hasReported()) {
|
|
623
|
+
terminalClose.reportNow(() => ev.emit('connection.update', {
|
|
624
|
+
connection: 'close',
|
|
625
|
+
lastDisconnect: {
|
|
626
|
+
error: logoutError,
|
|
627
|
+
date: new Date()
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
363
630
|
}
|
|
364
|
-
}
|
|
365
|
-
|
|
631
|
+
}
|
|
632
|
+
// The close is published on a chain one hop further out than the
|
|
633
|
+
// teardown both paths await, so without this `await sock.logout()`
|
|
634
|
+
// returns just before the event it caused.
|
|
635
|
+
//
|
|
636
|
+
// Skipped when re-entered from an end handler, for the same reason
|
|
637
|
+
// `end()` short-circuits there: the publish waits for the teardown, the
|
|
638
|
+
// teardown waits for the handler, and the handler would be waiting here
|
|
639
|
+
// — stalled until the watchdog fires.
|
|
640
|
+
if (!runningEndHandlers)
|
|
641
|
+
await terminalClose.published();
|
|
366
642
|
};
|
|
367
643
|
const registerSocketEndHandler = (handler) => {
|
|
368
644
|
socketEndHandlers.push(handler);
|
|
@@ -428,6 +704,39 @@ const makeWASocket = (config) => {
|
|
|
428
704
|
logger,
|
|
429
705
|
ws,
|
|
430
706
|
type: 'md',
|
|
707
|
+
/**
|
|
708
|
+
* `await using sock = makeWASocket(config)` — frees the wasm client on
|
|
709
|
+
* scope exit. Nothing else can: the bridge holds the JS event callbacks
|
|
710
|
+
* as wasm-bindgen externrefs and those callbacks reach back to this
|
|
711
|
+
* closure, so the reference cycle crosses the JS/wasm boundary and no
|
|
712
|
+
* `FinalizationRegistry` will ever fire for the client.
|
|
713
|
+
*
|
|
714
|
+
* Delegates to `end()`, so it flushes the auth store and is idempotent.
|
|
715
|
+
*
|
|
716
|
+
* Then awaits `initPromise`, because `end()` alone does not satisfy the
|
|
717
|
+
* async-disposal contract: called before `createWhatsAppClient()`
|
|
718
|
+
* settles, it sees `client === undefined`, frees nothing, and resolves
|
|
719
|
+
* while initialization is still running. What actually disposes that
|
|
720
|
+
* client is the `ended` guard inside `init()` — so code after the
|
|
721
|
+
* `await using` scope would otherwise overlap with bridge construction,
|
|
722
|
+
* event callbacks, and store access. `end()` runs first because it sets
|
|
723
|
+
* `ended` synchronously, which is what makes `init()` bail out early.
|
|
724
|
+
*
|
|
725
|
+
* The `finally` is load-bearing: `end()` rethrows the first auth-store
|
|
726
|
+
* flush failure, and letting that propagate directly would skip the wait
|
|
727
|
+
* and resolve the disposer with `init()` still in flight — losing the
|
|
728
|
+
* one guarantee it exists to provide, in exactly the situation where
|
|
729
|
+
* cleanup already went wrong. `initPromise` swallows its own failures,
|
|
730
|
+
* so awaiting it here cannot mask the flush error.
|
|
731
|
+
*/
|
|
732
|
+
async [Symbol.asyncDispose]() {
|
|
733
|
+
try {
|
|
734
|
+
await end(undefined);
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
await initPromise;
|
|
738
|
+
}
|
|
739
|
+
},
|
|
431
740
|
// Upstream `socket.ts:1106-1108` returns `authState.creds.me`, which
|
|
432
741
|
// carries `{ id, lid, name, verifiedName, ... }` — full Contact
|
|
433
742
|
// shape. Returning the bare `{id, lid}` like before broke
|
|
@@ -446,13 +755,13 @@ const makeWASocket = (config) => {
|
|
|
446
755
|
};
|
|
447
756
|
},
|
|
448
757
|
get waClient() {
|
|
449
|
-
return
|
|
758
|
+
return owner.peek();
|
|
450
759
|
},
|
|
451
760
|
get isConnected() {
|
|
452
|
-
return
|
|
761
|
+
return owner.peek()?.isConnected() ?? false;
|
|
453
762
|
},
|
|
454
763
|
get isLoggedIn() {
|
|
455
|
-
return
|
|
764
|
+
return owner.peek()?.isLoggedIn() ?? false;
|
|
456
765
|
},
|
|
457
766
|
get authState() {
|
|
458
767
|
return {
|
|
@@ -485,7 +794,17 @@ const makeWASocket = (config) => {
|
|
|
485
794
|
sendRawMessage: async (data) => {
|
|
486
795
|
return (await ctx.getClient()).sendRawMessage(data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
487
796
|
},
|
|
488
|
-
|
|
797
|
+
/**
|
|
798
|
+
* `dsmMessage` is accepted so the signature matches upstream, and
|
|
799
|
+
* refused rather than ignored. Upstream uses it to encrypt a different
|
|
800
|
+
* plaintext for the caller's own other devices; the engine encrypts one
|
|
801
|
+
* payload for every recipient, so honouring it is not possible here and
|
|
802
|
+
* dropping it would send those devices the wrong message.
|
|
803
|
+
*/
|
|
804
|
+
createParticipantNodes: async (jids, message, extraAttrs, dsmMessage) => {
|
|
805
|
+
if (dsmMessage) {
|
|
806
|
+
throw new Boom('createParticipantNodes: dsmMessage is not supported, the engine encrypts one payload for every recipient and cannot substitute a different one for your own devices', { statusCode: 501 });
|
|
807
|
+
}
|
|
489
808
|
const bytes = encodeProto('Message', message);
|
|
490
809
|
return (await ctx.getClient()).createParticipantNodesBytes(jids, bytes, extraAttrs ?? {});
|
|
491
810
|
},
|
|
@@ -496,7 +815,10 @@ const makeWASocket = (config) => {
|
|
|
496
815
|
registerSocketEndHandler,
|
|
497
816
|
waitForConnectionUpdate,
|
|
498
817
|
setAutoReconnect: (enabled) => {
|
|
499
|
-
|
|
818
|
+
// Mirrored locally because the dispatcher has to know: with this off,
|
|
819
|
+
// a plain drop is terminal rather than the start of a backoff.
|
|
820
|
+
autoReconnectEnabled = enabled;
|
|
821
|
+
owner.peek()?.setAutoReconnect(enabled);
|
|
500
822
|
},
|
|
501
823
|
/**
|
|
502
824
|
* Update presence either globally (`available`/`unavailable`) or per-chat
|
|
@@ -525,31 +847,7 @@ const makeWASocket = (config) => {
|
|
|
525
847
|
const bytes = data instanceof Uint8Array && !Buffer.isBuffer(data) ? data : new Uint8Array(data);
|
|
526
848
|
return (await ctx.getClient()).uploadMedia(bytes, toBridgeMediaType(opts.mediaType));
|
|
527
849
|
},
|
|
528
|
-
|
|
529
|
-
void force;
|
|
530
|
-
return (await ctx.getClient()).fetchPrivacySettings();
|
|
531
|
-
},
|
|
532
|
-
updatePrivacySetting: async (category, value) => {
|
|
533
|
-
await (await ctx.getClient()).updatePrivacySetting(category, value);
|
|
534
|
-
},
|
|
535
|
-
updateLastSeenPrivacy: async (value) => {
|
|
536
|
-
await (await ctx.getClient()).updatePrivacySetting('last', value);
|
|
537
|
-
},
|
|
538
|
-
updateOnlinePrivacy: async (value) => {
|
|
539
|
-
await (await ctx.getClient()).updatePrivacySetting('online', value);
|
|
540
|
-
},
|
|
541
|
-
updateProfilePicturePrivacy: async (value) => {
|
|
542
|
-
await (await ctx.getClient()).updatePrivacySetting('profile', value);
|
|
543
|
-
},
|
|
544
|
-
updateStatusPrivacy: async (value) => {
|
|
545
|
-
await (await ctx.getClient()).updatePrivacySetting('status', value);
|
|
546
|
-
},
|
|
547
|
-
updateReadReceiptsPrivacy: async (value) => {
|
|
548
|
-
await (await ctx.getClient()).updatePrivacySetting('readreceipts', value);
|
|
549
|
-
},
|
|
550
|
-
updateGroupsAddPrivacy: async (value) => {
|
|
551
|
-
await (await ctx.getClient()).updatePrivacySetting('groupadd', value);
|
|
552
|
-
},
|
|
850
|
+
...makePrivacyMethods(ctx),
|
|
553
851
|
updateDefaultDisappearingMode: async (duration) => {
|
|
554
852
|
await (await ctx.getClient()).updateDefaultDisappearingMode(duration);
|
|
555
853
|
},
|
|
@@ -590,11 +888,14 @@ const makeWASocket = (config) => {
|
|
|
590
888
|
...makeContactMethods(ctx),
|
|
591
889
|
...makeProfileMethods(ctx),
|
|
592
890
|
...makeChatActionMethods(ctx),
|
|
891
|
+
...makeInternalMethods(ctx),
|
|
593
892
|
...usyncMethods,
|
|
594
893
|
...makeStanzaResponseMethods(ctx),
|
|
595
894
|
...makePresenceMethods(ctx),
|
|
596
895
|
...makeBlockingMethods(ctx),
|
|
597
896
|
...makeNewsletterMethods(ctx),
|
|
897
|
+
...makeBusinessMethods(ctx),
|
|
898
|
+
...makeServerQueryMethods(ctx),
|
|
598
899
|
downloadMedia: async (message, type, options = {}) => {
|
|
599
900
|
return downloadMediaMessage(message, type, options, {
|
|
600
901
|
logger,
|
|
@@ -603,6 +904,16 @@ const makeWASocket = (config) => {
|
|
|
603
904
|
});
|
|
604
905
|
}
|
|
605
906
|
};
|
|
907
|
+
// Assigning replaces the handler the socket itself reports through, rather
|
|
908
|
+
// than shadowing it with a second one only a consumer could reach.
|
|
909
|
+
Object.defineProperty(sock, 'onUnexpectedError', {
|
|
910
|
+
get: () => unexpectedErrors.handler,
|
|
911
|
+
set: (handler) => {
|
|
912
|
+
unexpectedErrors.handler = handler;
|
|
913
|
+
},
|
|
914
|
+
enumerable: true,
|
|
915
|
+
configurable: true
|
|
916
|
+
});
|
|
606
917
|
return sock;
|
|
607
918
|
};
|
|
608
919
|
export default makeWASocket;
|