@violetix/baileys 0.4.0 → 0.5.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/lib/Defaults/index.d.ts +5 -0
- package/lib/Defaults/index.js +12 -2
- package/lib/Socket/Client/websocket.js +38 -3
- package/lib/Socket/chats.js +3 -1
- package/lib/Socket/groups.js +8 -3
- package/lib/Socket/index.d.ts +3 -0
- package/lib/Socket/messages-recv.js +3 -3
- package/lib/Socket/messages-send.js +16 -12
- package/lib/Socket/socket.js +203 -39
- package/lib/Store/make-in-memory-store.js +32 -24
- package/lib/Utils/chat-utils.js +5 -3
- package/lib/Utils/connection-manager.d.ts +44 -0
- package/lib/Utils/connection-manager.js +258 -0
- package/lib/Utils/event-buffer.js +4 -5
- package/lib/Utils/generics.js +21 -6
- package/lib/Utils/ignore-key.js +4 -2
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/messages-media.d.ts +2 -0
- package/lib/Utils/messages-media.js +3 -2
- package/lib/Utils/messages.js +2 -1
- package/lib/Utils/process-message.js +4 -1
- package/lib/Utils/rich-message-utils.js +1 -1
- package/package.json +1 -1
package/lib/Defaults/index.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export namespace DEFAULT_CONNECTION_CONFIG {
|
|
|
41
41
|
export let browser: any[];
|
|
42
42
|
export let waWebSocketUrl: string;
|
|
43
43
|
export let connectTimeoutMs: number;
|
|
44
|
+
export let closeTimeoutMs: number;
|
|
45
|
+
export let autoReconnect: boolean;
|
|
46
|
+
export let reconnectDelayMs: number;
|
|
47
|
+
export let maxReconnectDelayMs: number;
|
|
48
|
+
export let maxReconnectAttempts: number;
|
|
44
49
|
export let keepAliveIntervalMs: number;
|
|
45
50
|
export let logger: any;
|
|
46
51
|
export let emitOwnEvents: boolean;
|
package/lib/Defaults/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { proto } from '../../WAProto/index.js';
|
|
|
2
2
|
import { makeLibSignalRepository } from '../Signal/libsignal.js';
|
|
3
3
|
import { Browsers } from '../Utils/browser-utils.js';
|
|
4
4
|
import logger from '../Utils/logger.js';
|
|
5
|
-
const version = [2, 3000,
|
|
5
|
+
const version = [2, 3000, 1053032355];
|
|
6
6
|
export const UNAUTHORIZED_CODES = [401, 403, 419];
|
|
7
7
|
export const BIZ_BOT_SUPPORT_PAYLOAD = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}';
|
|
8
8
|
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com';
|
|
@@ -57,6 +57,14 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
57
57
|
browser: Browsers.macOS('Chrome'),
|
|
58
58
|
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
|
59
59
|
connectTimeoutMs: 20000,
|
|
60
|
+
// a half-open socket never answers close(); without a ceiling every reconnect leaks one
|
|
61
|
+
closeTimeoutMs: 5000,
|
|
62
|
+
// reconnect the transport in place instead of ending the socket, so a host that treats
|
|
63
|
+
// connection 'close' as "build a new socket" never runs two sockets for one account
|
|
64
|
+
autoReconnect: true,
|
|
65
|
+
reconnectDelayMs: 2000,
|
|
66
|
+
maxReconnectDelayMs: 60000,
|
|
67
|
+
maxReconnectAttempts: 10,
|
|
60
68
|
keepAliveIntervalMs: 30000,
|
|
61
69
|
keepAliveGracePeriodMs: 30000,
|
|
62
70
|
logger: logger.child({ class: 'baileys' }),
|
|
@@ -68,7 +76,9 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
68
76
|
fireInitQueries: true,
|
|
69
77
|
auth: undefined,
|
|
70
78
|
markOnlineOnConnect: true,
|
|
71
|
-
|
|
79
|
+
// FULL history is dropped by shouldSyncHistoryMessage below anyway; asking for it only adds
|
|
80
|
+
// reconnect load (upstream default is false too)
|
|
81
|
+
syncFullHistory: false,
|
|
72
82
|
patchMessageBeforeSending: msg => msg,
|
|
73
83
|
shouldSyncHistoryMessage: ({ syncType }) => {
|
|
74
84
|
return syncType !== proto.HistorySync.HistorySyncType.FULL;
|
|
@@ -35,16 +35,51 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
35
35
|
this.socket?.on(event, (...args) => this.emit(event, ...args));
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Drop the raw socket and open a new one on the SAME emitter. Every stanza listener the layers
|
|
40
|
+
* above registered (messages-recv, chats, the host) stays attached, which is what makes a
|
|
41
|
+
* reconnect invisible to them. The noise session is rebuilt by the caller.
|
|
42
|
+
*/
|
|
43
|
+
reconnect() {
|
|
44
|
+
const socket = this.socket;
|
|
45
|
+
this.socket = null;
|
|
46
|
+
if (socket) {
|
|
47
|
+
// remove the forwarders first: a late close from the old socket must not be reported
|
|
48
|
+
// as a close of the new one
|
|
49
|
+
socket.removeAllListeners();
|
|
50
|
+
try {
|
|
51
|
+
socket.terminate();
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
this.connect();
|
|
56
|
+
}
|
|
38
57
|
async close() {
|
|
39
58
|
if (!this.socket) {
|
|
40
59
|
return;
|
|
41
60
|
}
|
|
61
|
+
const socket = this.socket;
|
|
62
|
+
// A half-open peer never answers the close handshake, so an unbounded wait here leaks one
|
|
63
|
+
// socket per reconnect. With a fleet that ends as FD exhaustion, so bound it and terminate.
|
|
64
|
+
const timeoutMs = this.config.closeTimeoutMs ?? 5000;
|
|
42
65
|
const closePromise = new Promise(resolve => {
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
try {
|
|
68
|
+
socket.terminate();
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
resolve();
|
|
72
|
+
}, timeoutMs);
|
|
73
|
+
timer.unref?.();
|
|
74
|
+
const done = () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
socket.once('close', done);
|
|
79
|
+
socket.once('error', done);
|
|
45
80
|
});
|
|
46
81
|
try {
|
|
47
|
-
|
|
82
|
+
socket.close();
|
|
48
83
|
} catch { }
|
|
49
84
|
await closePromise;
|
|
50
85
|
this.socket = null;
|
package/lib/Socket/chats.js
CHANGED
|
@@ -1077,7 +1077,9 @@ export const makeChatsSocket = (config) => {
|
|
|
1077
1077
|
if (attrs.timestamp) {
|
|
1078
1078
|
let { lastAccountSyncTimestamp } = authState.creds;
|
|
1079
1079
|
if (lastAccountSyncTimestamp) {
|
|
1080
|
-
|
|
1080
|
+
// async ws handler: a rejection here would be unhandled and kill the process
|
|
1081
|
+
await cleanDirtyBits('account_sync', lastAccountSyncTimestamp)
|
|
1082
|
+
.catch(error => logger.warn({ error }, 'failed to clean account_sync dirty bits'));
|
|
1081
1083
|
}
|
|
1082
1084
|
lastAccountSyncTimestamp = +attrs.timestamp;
|
|
1083
1085
|
ev.emit('creds.update', { lastAccountSyncTimestamp });
|
package/lib/Socket/groups.js
CHANGED
|
@@ -23,7 +23,7 @@ export const makeGroupsSocket = (config) => {
|
|
|
23
23
|
}
|
|
24
24
|
const promise = groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
|
|
25
25
|
.then(result => extractGroupMetadata(result))
|
|
26
|
-
|
|
26
|
+
// no swallow: a null here made group sends go to zero recipients silently
|
|
27
27
|
.finally(() => inflightGroupMetadata.delete(jid));
|
|
28
28
|
inflightGroupMetadata.set(jid, promise);
|
|
29
29
|
return promise;
|
|
@@ -69,8 +69,13 @@ export const makeGroupsSocket = (config) => {
|
|
|
69
69
|
if (attrs.type !== 'groups') {
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
try {
|
|
73
|
+
await groupFetchAllParticipating();
|
|
74
|
+
await sock.cleanDirtyBits('groups');
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
config.logger?.warn({ error }, 'failed to handle dirty groups');
|
|
78
|
+
}
|
|
74
79
|
});
|
|
75
80
|
return {
|
|
76
81
|
...sock,
|
package/lib/Socket/index.d.ts
CHANGED
|
@@ -875,6 +875,8 @@ declare function makeWASocket(config: import('../Types/Socket.js').SocketConfig)
|
|
|
875
875
|
};
|
|
876
876
|
signalRepository: any;
|
|
877
877
|
user: any;
|
|
878
|
+
/** true while the transport is repairing itself; do not build a second socket for the account */
|
|
879
|
+
isReconnecting: boolean;
|
|
878
880
|
generateMessageTag: () => string;
|
|
879
881
|
/**
|
|
880
882
|
* Drops matching inbound stanzas before any handler — including the decryption and
|
|
@@ -890,6 +892,7 @@ declare function makeWASocket(config: import('../Types/Socket.js').SocketConfig)
|
|
|
890
892
|
logout: (msg: any) => Promise<void>;
|
|
891
893
|
end: (error: any) => Promise<void>;
|
|
892
894
|
registerSocketEndHandler: (handler: any) => void;
|
|
895
|
+
registerSocketReconnectHandler: (handler: any) => void;
|
|
893
896
|
onUnexpectedError: (err: any, msg: any) => void;
|
|
894
897
|
uploadPreKeys: (count?: number) => Promise<void>;
|
|
895
898
|
uploadPreKeysToServerIfRequired: () => Promise<void>;
|
|
@@ -5,7 +5,7 @@ import Long from 'long';
|
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
|
|
7
7
|
import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
-
import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, setBotMessageSecret, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
8
|
+
import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, setBotMessageSecret, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, unpadRandomMax16, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
10
|
import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
|
|
11
11
|
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
@@ -394,7 +394,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
394
394
|
}
|
|
395
395
|
};
|
|
396
396
|
const sendMessageAck = async (node, errorCode) => {
|
|
397
|
-
const stanza = buildAckStanza(node, errorCode, authState.creds.me
|
|
397
|
+
const stanza = buildAckStanza(node, errorCode, authState.creds.me?.id);
|
|
398
398
|
logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack');
|
|
399
399
|
await sendNode(stanza);
|
|
400
400
|
};
|
|
@@ -657,7 +657,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
657
657
|
};
|
|
658
658
|
break;
|
|
659
659
|
case 'modify':
|
|
660
|
-
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid);
|
|
660
|
+
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => JSON.stringify({ id: p.attrs.jid, phoneNumber: p.attrs.phone_number, lid: p.attrs.lid }));
|
|
661
661
|
msg.messageStubParameters = oldNumber || [];
|
|
662
662
|
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER;
|
|
663
663
|
break;
|
|
@@ -55,7 +55,7 @@ const getMediaTypeForGroupStatus = (media) => {
|
|
|
55
55
|
export const makeMessagesSocket = (config) => {
|
|
56
56
|
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
|
|
57
57
|
const sock = makeNewsletterSocket(config);
|
|
58
|
-
const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
|
|
58
|
+
const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler, registerSocketReconnectHandler } = sock;
|
|
59
59
|
/**
|
|
60
60
|
* Echo a message we just sent back into our own store, off the send path.
|
|
61
61
|
*
|
|
@@ -1242,6 +1242,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
1242
1242
|
messageRetryManager.clear();
|
|
1243
1243
|
}
|
|
1244
1244
|
});
|
|
1245
|
+
// the media host auth token is bound to the connection that fetched it
|
|
1246
|
+
registerSocketReconnectHandler?.(() => {
|
|
1247
|
+
mediaConn = undefined;
|
|
1248
|
+
});
|
|
1245
1249
|
const socket = {
|
|
1246
1250
|
...sock,
|
|
1247
1251
|
userDevicesCache,
|
|
@@ -1455,8 +1459,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
1455
1459
|
const isQuizMsg = 'poll' in content && !!content.poll.pollType;
|
|
1456
1460
|
const isAiMsg = 'ai' in content && !!content.ai;
|
|
1457
1461
|
const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
|
|
1458
|
-
|
|
1459
|
-
|
|
1462
|
+
// copies: these get .edit / meta / bot added below, which must not leak into the
|
|
1463
|
+
// caller's reused options object (stacked nodes, stray edit turning sends into deletes)
|
|
1464
|
+
const additionalAttributes = { ...options.additionalAttributes };
|
|
1465
|
+
const additionalNodes = [...(options.additionalNodes || [])];
|
|
1460
1466
|
// required for delete
|
|
1461
1467
|
if (isDeleteMsg || isKeepMsg) {
|
|
1462
1468
|
// if the chat is a group, and I am not the author, then delete the message as an admin
|
|
@@ -1512,7 +1518,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
1512
1518
|
},
|
|
1513
1519
|
content: undefined
|
|
1514
1520
|
});
|
|
1515
|
-
delete content.ai;
|
|
1516
1521
|
}
|
|
1517
1522
|
const relayOpts = {
|
|
1518
1523
|
messageId: fullMsg.key.id,
|
|
@@ -1523,12 +1528,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
1523
1528
|
additionalAttributes,
|
|
1524
1529
|
additionalNodes
|
|
1525
1530
|
};
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1531
|
+
// children (poll option images, album items) must honour `exclusive` too
|
|
1532
|
+
const relay = (message, opts) => options.exclusive
|
|
1533
|
+
? relayExclusive(jid, message, options, opts)
|
|
1534
|
+
: relayMessage(jid, message, opts);
|
|
1535
|
+
await relay(fullMsg.message, relayOpts);
|
|
1532
1536
|
if (config.emitOwnEvents) {
|
|
1533
1537
|
setImmediate(() => emitOwnUpsert(fullMsg));
|
|
1534
1538
|
}
|
|
@@ -1548,7 +1552,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
1548
1552
|
userJid,
|
|
1549
1553
|
messageId: generateMessageIDV2(userJid)
|
|
1550
1554
|
});
|
|
1551
|
-
await
|
|
1555
|
+
await relay(optionMsg.message, {
|
|
1552
1556
|
messageId: optionMsg.key.id,
|
|
1553
1557
|
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1554
1558
|
additionalAttributes,
|
|
@@ -1581,7 +1585,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
1581
1585
|
parentMessageKey: fullMsg.key,
|
|
1582
1586
|
associationType: AssociationType.MEDIA_ALBUM
|
|
1583
1587
|
};
|
|
1584
|
-
await
|
|
1588
|
+
await relay(albumMsg.message, {
|
|
1585
1589
|
messageId: albumMsg.key.id,
|
|
1586
1590
|
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1587
1591
|
addBizAttributes: isNeedBizAttrs,
|
package/lib/Socket/socket.js
CHANGED
|
@@ -27,6 +27,17 @@ import { executeWMexQuery } from './mex.js';
|
|
|
27
27
|
* so a stale hardcoded version heals on the next reconnect instead of looping on 405.
|
|
28
28
|
*/
|
|
29
29
|
let latestVersionOverride;
|
|
30
|
+
/**
|
|
31
|
+
* Reasons that end a session for good. Reconnecting any of these is how an unattended host turns
|
|
32
|
+
* one logout into an auth loop and gets the number banned.
|
|
33
|
+
*/
|
|
34
|
+
const DEAD_DISCONNECT_REASONS = new Set([
|
|
35
|
+
DisconnectReason.loggedOut,
|
|
36
|
+
DisconnectReason.forbidden,
|
|
37
|
+
DisconnectReason.multideviceMismatch,
|
|
38
|
+
DisconnectReason.badSession,
|
|
39
|
+
DisconnectReason.connectionReplaced
|
|
40
|
+
]);
|
|
30
41
|
export const makeSocket = (config) => {
|
|
31
42
|
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, keepAliveGracePeriodMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
|
|
32
43
|
const publicWAMBuffer = new BinaryInfo();
|
|
@@ -52,9 +63,9 @@ export const makeSocket = (config) => {
|
|
|
52
63
|
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
|
|
53
64
|
}
|
|
54
65
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
|
55
|
-
|
|
66
|
+
let ephemeralKeyPair = Curve.generateKeyPair();
|
|
56
67
|
/** WA noise protocol wrapper */
|
|
57
|
-
|
|
68
|
+
let noise = makeNoiseHandler({
|
|
58
69
|
keyPair: ephemeralKeyPair,
|
|
59
70
|
NOISE_HEADER: NOISE_WA_HEADER,
|
|
60
71
|
logger,
|
|
@@ -136,12 +147,13 @@ export const makeSocket = (config) => {
|
|
|
136
147
|
node.attrs.id = generateMessageTag();
|
|
137
148
|
}
|
|
138
149
|
const msgId = node.attrs.id;
|
|
150
|
+
pendingQueries++;
|
|
139
151
|
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
140
152
|
const result = waitForMessage(msgId, timeoutMs).catch(reject);
|
|
141
153
|
sendNode(node)
|
|
142
154
|
.then(async () => resolve(await result))
|
|
143
155
|
.catch(reject);
|
|
144
|
-
});
|
|
156
|
+
}).finally(() => { pendingQueries--; });
|
|
145
157
|
if (result && 'tag' in result) {
|
|
146
158
|
assertNodeErrorFree(result);
|
|
147
159
|
}
|
|
@@ -376,10 +388,17 @@ export const makeSocket = (config) => {
|
|
|
376
388
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
|
|
377
389
|
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
|
|
378
390
|
let lastDateRecv;
|
|
391
|
+
/** queries awaiting a reply; keep-alive skips its ping while these are flowing */
|
|
392
|
+
let pendingQueries = 0;
|
|
379
393
|
let epoch = 1;
|
|
380
394
|
let keepAliveReq;
|
|
381
395
|
let qrTimer;
|
|
382
396
|
let closed = false;
|
|
397
|
+
/** transparent transport reconnect state; see scheduleReconnect */
|
|
398
|
+
let reconnecting = false;
|
|
399
|
+
let reconnectAttempts = 0;
|
|
400
|
+
let reconnectTimer;
|
|
401
|
+
let reconnectWaitResolve;
|
|
383
402
|
/** set once WhatsApp sends pair-device; requestPairingCode queues until then */
|
|
384
403
|
let pairingReady = false;
|
|
385
404
|
let pairingInProgress = false;
|
|
@@ -388,6 +407,8 @@ export const makeSocket = (config) => {
|
|
|
388
407
|
let pendingPairingResolve;
|
|
389
408
|
let pendingPairingReject;
|
|
390
409
|
const socketEndHandlers = [];
|
|
410
|
+
/** per-connection caches (media host auth, ...) that must be dropped when the transport reopens */
|
|
411
|
+
const socketReconnectHandlers = [];
|
|
391
412
|
const timers = new Set();
|
|
392
413
|
const safeSetTimeout = (fn, ms) => { const id = setTimeout(fn, ms); timers.add(id); return id; };
|
|
393
414
|
const safeSetInterval = (fn, ms) => { const id = setInterval(fn, ms); timers.add(id); return id; };
|
|
@@ -647,6 +668,9 @@ export const makeSocket = (config) => {
|
|
|
647
668
|
keepAliveReq?.cancel();
|
|
648
669
|
clearSafeTimer(qrTimer);
|
|
649
670
|
clearAllTimers();
|
|
671
|
+
// release a pending reconnect wait so scheduleReconnect cannot stay suspended
|
|
672
|
+
reconnectWaitResolve?.();
|
|
673
|
+
reconnectWaitResolve = undefined;
|
|
650
674
|
// a queued requestPairingCode would hang forever otherwise
|
|
651
675
|
pairingReady = false;
|
|
652
676
|
pairingInProgress = false;
|
|
@@ -690,6 +714,90 @@ export const makeSocket = (config) => {
|
|
|
690
714
|
logger.error({ err }, 'error while closing connection');
|
|
691
715
|
}
|
|
692
716
|
};
|
|
717
|
+
/** rebuild the state that belongs to one transport connection, not to the socket object */
|
|
718
|
+
const resetTransport = () => {
|
|
719
|
+
keepAliveReq?.cancel();
|
|
720
|
+
keepAliveReq = undefined;
|
|
721
|
+
clearSafeTimer(qrTimer);
|
|
722
|
+
qrTimer = undefined;
|
|
723
|
+
lastDateRecv = undefined;
|
|
724
|
+
ephemeralKeyPair = Curve.generateKeyPair();
|
|
725
|
+
noise = makeNoiseHandler({
|
|
726
|
+
keyPair: ephemeralKeyPair,
|
|
727
|
+
NOISE_HEADER: NOISE_WA_HEADER,
|
|
728
|
+
logger,
|
|
729
|
+
routingInfo: authState?.creds?.routingInfo
|
|
730
|
+
});
|
|
731
|
+
// the media host token is issued per connection; a cached one fails every upload after this
|
|
732
|
+
for (const handler of socketReconnectHandlers) {
|
|
733
|
+
try {
|
|
734
|
+
handler();
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
logger.error({ err }, 'error in socket reconnect handler');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
/**
|
|
742
|
+
* Transparent reconnect. The WebSocket client object is kept and only the raw socket and the
|
|
743
|
+
* noise session under it are replaced, so every stanza listener registered by the layers above
|
|
744
|
+
* (messages-recv, chats, the host) keeps working and the host never sees a `close` for a drop
|
|
745
|
+
* that this socket can repair itself.
|
|
746
|
+
*/
|
|
747
|
+
const scheduleReconnect = async (error) => {
|
|
748
|
+
if (reconnecting || closed) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
reconnecting = true;
|
|
752
|
+
const statusCode = error?.output?.statusCode;
|
|
753
|
+
const maxAttempts = config.maxReconnectAttempts ?? 10;
|
|
754
|
+
if (!config.autoReconnect || DEAD_DISCONNECT_REASONS.has(statusCode) || reconnectAttempts >= maxAttempts) {
|
|
755
|
+
reconnecting = false;
|
|
756
|
+
return end(error);
|
|
757
|
+
}
|
|
758
|
+
reconnectAttempts += 1;
|
|
759
|
+
// 515 means the server wants the connection back now; backing off only delays it
|
|
760
|
+
const immediate = statusCode === DisconnectReason.restartRequired;
|
|
761
|
+
const base = config.reconnectDelayMs ?? 2000;
|
|
762
|
+
const ceiling = config.maxReconnectDelayMs ?? 60000;
|
|
763
|
+
const backoff = Math.min(ceiling, base * 2 ** (reconnectAttempts - 1));
|
|
764
|
+
// jitter keeps a fleet from reconnecting in lockstep after a server-side blip
|
|
765
|
+
const delayMs = immediate ? 0 : Math.round(backoff * (0.75 + Math.random() * 0.5));
|
|
766
|
+
logger.info({ statusCode, attempt: reconnectAttempts, delayMs }, 'connection lost, reconnecting');
|
|
767
|
+
// NOT 'close': a host reads close as "build a new socket", and a second socket for the same
|
|
768
|
+
// account is what earns a 440 connectionReplaced
|
|
769
|
+
ev.emit('connection.update', {
|
|
770
|
+
connection: 'connecting',
|
|
771
|
+
lastDisconnect: { error, date: new Date() }
|
|
772
|
+
});
|
|
773
|
+
await new Promise(resolve => {
|
|
774
|
+
reconnectWaitResolve = resolve;
|
|
775
|
+
reconnectTimer = safeSetTimeout(() => {
|
|
776
|
+
reconnectWaitResolve = undefined;
|
|
777
|
+
resolve();
|
|
778
|
+
}, delayMs);
|
|
779
|
+
});
|
|
780
|
+
reconnectTimer = undefined;
|
|
781
|
+
reconnecting = false;
|
|
782
|
+
if (closed) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
resetTransport();
|
|
787
|
+
ws.reconnect();
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
logger.error({ err }, 'failed to reopen the transport');
|
|
791
|
+
return end(err);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
/** single entry point for every transport-level close */
|
|
795
|
+
const handleDisconnect = (error) => {
|
|
796
|
+
if (closed) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
return scheduleReconnect(error);
|
|
800
|
+
};
|
|
693
801
|
const waitForSocketOpen = async () => {
|
|
694
802
|
if (ws.isOpen) {
|
|
695
803
|
return;
|
|
@@ -711,48 +819,89 @@ export const makeSocket = (config) => {
|
|
|
711
819
|
ws.off('error', onClose);
|
|
712
820
|
});
|
|
713
821
|
};
|
|
822
|
+
/**
|
|
823
|
+
* Keep-alive, modelled on zapo-js (src/transport/keepalive/WaKeepAlive.ts):
|
|
824
|
+
* - self-rescheduling setTimeout with +-10% jitter (min 250ms), so a fleet of sockets in one
|
|
825
|
+
* process drifts apart instead of pinging in the same tick
|
|
826
|
+
* - at most one ping in flight; a slow pong never stacks more pings on a busy loop
|
|
827
|
+
* - no ping while other queries are pending and data arrived recently: the link is
|
|
828
|
+
* demonstrably alive and the ping would only add queue pressure
|
|
829
|
+
* - "lost" is still decided by inbound silence (interval + grace), and skipped when the
|
|
830
|
+
* timer itself fired late (blocked event loop: pongs may be unread in the socket buffer)
|
|
831
|
+
*/
|
|
714
832
|
const startKeepAliveRequest = () => {
|
|
833
|
+
let timer;
|
|
834
|
+
let stopped = false;
|
|
835
|
+
let pingInFlight = false;
|
|
836
|
+
let lastCheckAt;
|
|
837
|
+
let expectedDelayMs = 0;
|
|
838
|
+
const nextDelay = () => {
|
|
839
|
+
const jitter = Math.max(250, Math.floor(keepAliveIntervalMs * 0.1));
|
|
840
|
+
return Math.max(1000, keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter));
|
|
841
|
+
};
|
|
842
|
+
const schedule = (delayMs) => {
|
|
843
|
+
if (stopped) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
expectedDelayMs = delayMs;
|
|
847
|
+
timer = safeSetTimeout(() => {
|
|
848
|
+
timer = undefined;
|
|
849
|
+
cb();
|
|
850
|
+
schedule(nextDelay());
|
|
851
|
+
}, delayMs);
|
|
852
|
+
};
|
|
715
853
|
const cb = () => {
|
|
716
854
|
if (!lastDateRecv) {
|
|
717
855
|
lastDateRecv = new Date();
|
|
718
856
|
}
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
857
|
+
const now = Date.now();
|
|
858
|
+
const sinceRecvMs = now - lastDateRecv.getTime();
|
|
859
|
+
const lagMs = lastCheckAt ? now - lastCheckAt - expectedDelayMs : 0;
|
|
860
|
+
lastCheckAt = now;
|
|
861
|
+
const loopStalled = lagMs > keepAliveGracePeriodMs;
|
|
862
|
+
if (loopStalled) {
|
|
863
|
+
logger.warn({ lagMs, sinceLastRecvMs: sinceRecvMs }, 'event loop stalled; deferring keep-alive timeout');
|
|
722
864
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
attrs: {
|
|
727
|
-
id: generateMessageTag(),
|
|
728
|
-
to: S_WHATSAPP_NET,
|
|
729
|
-
type: 'get',
|
|
730
|
-
xmlns: 'w:p'
|
|
731
|
-
},
|
|
732
|
-
content: [{ tag: 'ping', attrs: {} }]
|
|
733
|
-
}).catch(err => {
|
|
734
|
-
logger.error({ trace: err.stack }, 'error in sending keep alive');
|
|
735
|
-
});
|
|
865
|
+
if (sinceRecvMs > keepAliveIntervalMs + keepAliveGracePeriodMs && !loopStalled) {
|
|
866
|
+
void handleDisconnect(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
867
|
+
return;
|
|
736
868
|
}
|
|
737
|
-
|
|
869
|
+
if (!ws.isOpen) {
|
|
738
870
|
logger.warn('keep alive called when WS not open');
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (pingInFlight) {
|
|
874
|
+
logger.trace('keep alive skipped: ping already in flight');
|
|
875
|
+
return;
|
|
739
876
|
}
|
|
877
|
+
if (pendingQueries > 0 && sinceRecvMs < keepAliveGracePeriodMs) {
|
|
878
|
+
logger.trace({ pendingQueries, sinceRecvMs }, 'keep alive skipped: queries pending with recent inbound');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
pingInFlight = true;
|
|
882
|
+
query({
|
|
883
|
+
tag: 'iq',
|
|
884
|
+
attrs: {
|
|
885
|
+
id: generateMessageTag(),
|
|
886
|
+
to: S_WHATSAPP_NET,
|
|
887
|
+
type: 'get',
|
|
888
|
+
xmlns: 'w:p'
|
|
889
|
+
},
|
|
890
|
+
content: [{ tag: 'ping', attrs: {} }]
|
|
891
|
+
}, keepAliveIntervalMs + keepAliveGracePeriodMs)
|
|
892
|
+
.catch(err => {
|
|
893
|
+
// the verdict stays with inbound silence above; a failed ping alone is not "lost"
|
|
894
|
+
logger.warn({ trace: err.stack }, 'error in sending keep alive');
|
|
895
|
+
})
|
|
896
|
+
.finally(() => { pingInFlight = false; });
|
|
740
897
|
};
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
// used the configured value, so any interval other than 30s produced false connectionLost.
|
|
744
|
-
// The random start offset staggers the fleet - on the shared timer every socket in the
|
|
745
|
-
// process pinged in the same tick, which is a self-inflicted burst at 100+ sessions.
|
|
746
|
-
let pinger;
|
|
747
|
-
const starter = safeSetTimeout(() => {
|
|
748
|
-
cb();
|
|
749
|
-
pinger = safeSetInterval(cb, keepAliveIntervalMs);
|
|
750
|
-
}, Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
|
|
898
|
+
// random first tick staggers sockets that connect together (reconnect storms)
|
|
899
|
+
schedule(Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
|
|
751
900
|
keepAliveReq = {
|
|
752
901
|
cb,
|
|
753
902
|
cancel: () => {
|
|
754
|
-
|
|
755
|
-
clearSafeTimer(
|
|
903
|
+
stopped = true;
|
|
904
|
+
clearSafeTimer(timer);
|
|
756
905
|
}
|
|
757
906
|
};
|
|
758
907
|
};
|
|
@@ -944,13 +1093,13 @@ export const makeSocket = (config) => {
|
|
|
944
1093
|
}
|
|
945
1094
|
catch (err) {
|
|
946
1095
|
logger.error({ err }, 'error in validating connection');
|
|
947
|
-
void
|
|
1096
|
+
void handleDisconnect(err);
|
|
948
1097
|
}
|
|
949
1098
|
});
|
|
950
|
-
ws.on('error', mapWebSocketError(
|
|
951
|
-
ws.on('close', () => void
|
|
1099
|
+
ws.on('error', mapWebSocketError(handleDisconnect));
|
|
1100
|
+
ws.on('close', () => void handleDisconnect(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
952
1101
|
// the server terminated the connection
|
|
953
|
-
ws.on('CB:xmlstreamend', () => void
|
|
1102
|
+
ws.on('CB:xmlstreamend', () => void handleDisconnect(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
954
1103
|
// QR gen
|
|
955
1104
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
956
1105
|
const iq = {
|
|
@@ -1015,6 +1164,9 @@ export const makeSocket = (config) => {
|
|
|
1015
1164
|
});
|
|
1016
1165
|
// login complete
|
|
1017
1166
|
ws.on('CB:success', async (node) => {
|
|
1167
|
+
// a completed handshake clears the reconnect budget: the next drop starts from the base delay
|
|
1168
|
+
reconnectAttempts = 0;
|
|
1169
|
+
reconnecting = false;
|
|
1018
1170
|
try {
|
|
1019
1171
|
updateServerTimeOffset(node);
|
|
1020
1172
|
await uploadPreKeysToServerIfRequired();
|
|
@@ -1063,7 +1215,7 @@ export const makeSocket = (config) => {
|
|
|
1063
1215
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
1064
1216
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
1065
1217
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
1066
|
-
void
|
|
1218
|
+
void handleDisconnect(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
1067
1219
|
});
|
|
1068
1220
|
// stream fail, possible logout
|
|
1069
1221
|
ws.on('CB:failure', async (node) => {
|
|
@@ -1079,10 +1231,10 @@ export const makeSocket = (config) => {
|
|
|
1079
1231
|
logger.warn({ err: latest.error?.message }, 'server rejected the client version as too old (405) and the latest version could not be fetched; pass a current `version`');
|
|
1080
1232
|
}
|
|
1081
1233
|
}
|
|
1082
|
-
void
|
|
1234
|
+
void handleDisconnect(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
1083
1235
|
});
|
|
1084
1236
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
1085
|
-
void
|
|
1237
|
+
void handleDisconnect(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
1086
1238
|
});
|
|
1087
1239
|
// The server delivers the offline queue in windows and waits for another <offline_batch/>
|
|
1088
1240
|
// before sending the next one. A single request only drained the first 100 stanzas, so a
|
|
@@ -1211,6 +1363,9 @@ export const makeSocket = (config) => {
|
|
|
1211
1363
|
const registerSocketEndHandler = (handler) => {
|
|
1212
1364
|
socketEndHandlers.push(handler);
|
|
1213
1365
|
};
|
|
1366
|
+
const registerSocketReconnectHandler = (handler) => {
|
|
1367
|
+
socketReconnectHandlers.push(handler);
|
|
1368
|
+
};
|
|
1214
1369
|
/**
|
|
1215
1370
|
* Fetches your account's standing when it comes to restrictions.
|
|
1216
1371
|
* @returns Returns the state of the restrictions.
|
|
@@ -1243,6 +1398,14 @@ export const makeSocket = (config) => {
|
|
|
1243
1398
|
get user() {
|
|
1244
1399
|
return authState.creds.me;
|
|
1245
1400
|
},
|
|
1401
|
+
/**
|
|
1402
|
+
* True while the transport is repairing itself. A host that reads this does not build a
|
|
1403
|
+
* second socket for the same account, which is what a `ws.isOpen === false` check would
|
|
1404
|
+
* make it do during the backoff window and the handshake that follows.
|
|
1405
|
+
*/
|
|
1406
|
+
get isReconnecting() {
|
|
1407
|
+
return reconnecting || (reconnectAttempts > 0 && !closed && !ws.isOpen);
|
|
1408
|
+
},
|
|
1246
1409
|
generateMessageTag,
|
|
1247
1410
|
ignoreKey: ignoreKeyRegistry.register,
|
|
1248
1411
|
query,
|
|
@@ -1253,6 +1416,7 @@ export const makeSocket = (config) => {
|
|
|
1253
1416
|
logout,
|
|
1254
1417
|
end,
|
|
1255
1418
|
registerSocketEndHandler,
|
|
1419
|
+
registerSocketReconnectHandler,
|
|
1256
1420
|
onUnexpectedError,
|
|
1257
1421
|
uploadPreKeys,
|
|
1258
1422
|
uploadPreKeysToServerIfRequired,
|
|
@@ -9,7 +9,7 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
|
9
9
|
import { WAProto } from '../Types/index.js';
|
|
10
10
|
import { LabelAssociationType } from '../Types/LabelAssociation.js';
|
|
11
11
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
12
|
-
import { md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
|
|
12
|
+
import { BufferJSON, md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
|
|
13
13
|
import { jidDecode, jidNormalizedUser } from '../WABinary/index.js';
|
|
14
14
|
import { makeOrderedDictionary } from './make-ordered-dictionary.js';
|
|
15
15
|
import { ObjectRepository } from './object-repository.js';
|
|
@@ -69,6 +69,9 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
// one key rule for every handler; upsert used alt+normalized while update/receipt/reaction/delete
|
|
73
|
+
// used raw or non-alt keys, so LID chats never found their stored messages
|
|
74
|
+
const keyJid = (key) => jidNormalizedUser(key.remoteJidAlt || key.remoteJid);
|
|
72
75
|
const assertMessageList = (jid) => {
|
|
73
76
|
if (!messages[jid]) {
|
|
74
77
|
messages[jid] = makeMessagesDictionary();
|
|
@@ -116,8 +119,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
116
119
|
}
|
|
117
120
|
logger.debug({ deletedContacts: isLatest ? oldContacts.size : 0, newContacts }, 'synced contacts');
|
|
118
121
|
for (const msg of newMessages) {
|
|
119
|
-
const
|
|
120
|
-
const list = assertMessageList(jid);
|
|
122
|
+
const list = assertMessageList(keyJid(msg.key));
|
|
121
123
|
list.upsert(msg, 'prepend');
|
|
122
124
|
}
|
|
123
125
|
enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
|
|
@@ -149,9 +151,13 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
else {
|
|
152
|
-
|
|
154
|
+
logger.debug({ update }, 'got update for non-existant contact');
|
|
155
|
+
continue;
|
|
153
156
|
}
|
|
154
|
-
Object.assign(
|
|
157
|
+
// was Object.assign(contact, contact): update fields (notify, name...) never landed.
|
|
158
|
+
// id may be a WA_ADD_NOTIF hash and imgUrl a sentinel, so neither is copied.
|
|
159
|
+
const { id: _id, imgUrl: _imgUrl, ...fields } = update;
|
|
160
|
+
Object.assign(contact, fields);
|
|
155
161
|
}
|
|
156
162
|
});
|
|
157
163
|
on('chats.upsert', newChats => {
|
|
@@ -211,7 +217,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
211
217
|
case 'append':
|
|
212
218
|
case 'notify':
|
|
213
219
|
for (const msg of newMessages) {
|
|
214
|
-
const jid =
|
|
220
|
+
const jid = keyJid(msg.key);
|
|
215
221
|
const list = assertMessageList(jid);
|
|
216
222
|
list.upsert(msg, 'append');
|
|
217
223
|
enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
|
|
@@ -231,7 +237,12 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
231
237
|
on('messages.update', updates => {
|
|
232
238
|
var _a;
|
|
233
239
|
for (const { update, key } of updates) {
|
|
234
|
-
|
|
240
|
+
// no create: an update for an unknown chat must not add an empty list that evicts real ones
|
|
241
|
+
const list = messages[keyJid(key)];
|
|
242
|
+
if (!list) {
|
|
243
|
+
logger.debug({ update }, 'got update for non-existent message');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
235
246
|
if (update === null || update === void 0 ? void 0 : update.status) {
|
|
236
247
|
const listStatus = (_a = list.get(key.id)) === null || _a === void 0 ? void 0 : _a.status;
|
|
237
248
|
if (listStatus && (update === null || update === void 0 ? void 0 : update.status) <= listStatus) {
|
|
@@ -252,8 +263,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
252
263
|
list === null || list === void 0 ? void 0 : list.clear();
|
|
253
264
|
}
|
|
254
265
|
else {
|
|
255
|
-
const
|
|
256
|
-
const list = messages[jid];
|
|
266
|
+
const list = messages[keyJid(item.keys[0])];
|
|
257
267
|
if (list) {
|
|
258
268
|
const idSet = new Set(item.keys.map(k => k.id));
|
|
259
269
|
list.filter(m => !idSet.has(m.key.id));
|
|
@@ -308,8 +318,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
308
318
|
});
|
|
309
319
|
on('message-receipt.update', updates => {
|
|
310
320
|
for (const { key, receipt } of updates) {
|
|
311
|
-
const
|
|
312
|
-
const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
|
|
321
|
+
const msg = messages[keyJid(key)]?.get(key.id);
|
|
313
322
|
if (msg) {
|
|
314
323
|
updateMessageWithReceipt(msg, receipt);
|
|
315
324
|
}
|
|
@@ -317,8 +326,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
317
326
|
});
|
|
318
327
|
on('messages.reaction', (reactions) => {
|
|
319
328
|
for (const { key, reaction } of reactions) {
|
|
320
|
-
const
|
|
321
|
-
const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
|
|
329
|
+
const msg = messages[keyJid(key)]?.get(key.id);
|
|
322
330
|
if (msg) {
|
|
323
331
|
updateMessageWithReaction(msg, reaction);
|
|
324
332
|
}
|
|
@@ -363,28 +371,28 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
363
371
|
bind,
|
|
364
372
|
/** loads messages from the store, if not found -- uses the legacy connection */
|
|
365
373
|
loadMessages: async (jid, count, cursor) => {
|
|
366
|
-
const list =
|
|
374
|
+
const list = messages[jid]; // read-only: don't create phantom lists
|
|
367
375
|
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
|
|
368
376
|
const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
|
|
369
|
-
const cursorValue = cursorKey ? list
|
|
370
|
-
let
|
|
377
|
+
const cursorValue = cursorKey ? list?.get(cursorKey.id) : undefined;
|
|
378
|
+
let result;
|
|
371
379
|
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
|
372
380
|
if (cursorValue) {
|
|
373
381
|
const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
|
|
374
|
-
|
|
382
|
+
result = list.array.slice(0, msgIdx);
|
|
375
383
|
}
|
|
376
384
|
else {
|
|
377
|
-
|
|
385
|
+
result = list.array;
|
|
378
386
|
}
|
|
379
|
-
const diff = count -
|
|
387
|
+
const diff = count - result.length;
|
|
380
388
|
if (diff < 0) {
|
|
381
|
-
|
|
389
|
+
result = result.slice(-count); // get the last X messages
|
|
382
390
|
}
|
|
383
391
|
}
|
|
384
392
|
else {
|
|
385
|
-
|
|
393
|
+
result = [];
|
|
386
394
|
}
|
|
387
|
-
return
|
|
395
|
+
return result;
|
|
388
396
|
},
|
|
389
397
|
/**
|
|
390
398
|
* Get all available labels for profile
|
|
@@ -447,13 +455,13 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
447
455
|
toJSON,
|
|
448
456
|
fromJSON,
|
|
449
457
|
writeToFile: (path) => {
|
|
450
|
-
writeFileSync(path, JSON.stringify(toJSON()));
|
|
458
|
+
writeFileSync(path, JSON.stringify(toJSON(), BufferJSON.replacer));
|
|
451
459
|
},
|
|
452
460
|
readFromFile: (path) => {
|
|
453
461
|
if (existsSync(path)) {
|
|
454
462
|
logger.debug({ path }, 'reading from file');
|
|
455
463
|
const jsonStr = readFileSync(path, { encoding: 'utf-8' });
|
|
456
|
-
const json = JSON.parse(jsonStr);
|
|
464
|
+
const json = JSON.parse(jsonStr, BufferJSON.reviver);
|
|
457
465
|
fromJSON(json);
|
|
458
466
|
}
|
|
459
467
|
}
|
package/lib/Utils/chat-utils.js
CHANGED
|
@@ -344,7 +344,6 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
344
344
|
syncd.mutations?.push(...ref.mutations);
|
|
345
345
|
}
|
|
346
346
|
const patchVersion = toNumber(version.version);
|
|
347
|
-
newState.version = patchVersion;
|
|
348
347
|
const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber;
|
|
349
348
|
let decodeResult;
|
|
350
349
|
try {
|
|
@@ -358,9 +357,12 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
358
357
|
catch (err) {
|
|
359
358
|
if (isMissingKeyError(err))
|
|
360
359
|
throw err;
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
// stop here: advancing the version past an undecoded patch loses its mutations for good;
|
|
361
|
+
// the next sync resumes from the last good version
|
|
362
|
+
logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, stopping');
|
|
363
|
+
break;
|
|
363
364
|
}
|
|
365
|
+
newState.version = patchVersion;
|
|
364
366
|
newState.hash = decodeResult.hash;
|
|
365
367
|
newState.indexValueMap = decodeResult.indexValueMap;
|
|
366
368
|
if (validateMacs) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Agent } from "https";
|
|
3
|
+
export type ConnectionManagerSession = {
|
|
4
|
+
id: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
export type ConnectionManagerOptions = {
|
|
8
|
+
/** Build one socket for a session. Called again for every reconnect. */
|
|
9
|
+
create: (session: ConnectionManagerSession) => Promise<any> | any;
|
|
10
|
+
sessions?: ConnectionManagerSession[];
|
|
11
|
+
maxConcurrent?: number;
|
|
12
|
+
reconnect?: boolean;
|
|
13
|
+
baseDelayMs?: number;
|
|
14
|
+
maxDelayMs?: number;
|
|
15
|
+
maxAttempts?: number;
|
|
16
|
+
staggerMs?: number;
|
|
17
|
+
logger?: any;
|
|
18
|
+
};
|
|
19
|
+
export type ConnectionManagerStats = {
|
|
20
|
+
total: number;
|
|
21
|
+
idle: number;
|
|
22
|
+
connecting: number;
|
|
23
|
+
open: number;
|
|
24
|
+
reconnecting: number;
|
|
25
|
+
closed: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
activeConnects: number;
|
|
28
|
+
queuedConnects: number;
|
|
29
|
+
};
|
|
30
|
+
export declare class ConnectionManager extends EventEmitter {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options: ConnectionManagerOptions);
|
|
33
|
+
start(): ConnectionManagerStats;
|
|
34
|
+
get(id: string): any | null;
|
|
35
|
+
stop(id: string): boolean;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
stats(): ConnectionManagerStats;
|
|
38
|
+
}
|
|
39
|
+
export declare const makeConnectionManager: (options: ConnectionManagerOptions) => ConnectionManager;
|
|
40
|
+
export declare const makeWssAgent: (options?: {
|
|
41
|
+
maxSockets?: number;
|
|
42
|
+
keepAlive?: boolean;
|
|
43
|
+
keepAliveMsecs?: number;
|
|
44
|
+
}) => Agent;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Agent } from 'https';
|
|
3
|
+
import { DisconnectReason } from '../Types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Supervises a fleet of sockets inside one process.
|
|
6
|
+
*
|
|
7
|
+
* The library builds and tears down one socket; it deliberately does not reconnect, because only
|
|
8
|
+
* the host knows which auth state to reload. With hundreds of accounts that leaves three fleet-level
|
|
9
|
+
* problems to whoever wires them up, and getting them wrong is what makes accounts drop:
|
|
10
|
+
*
|
|
11
|
+
* - every socket reconnecting the moment the server closes it, all at once, which hammers the
|
|
12
|
+
* endpoint and gets throttled or banned (this is what `baseDelayMs` + jitter + stagger fix);
|
|
13
|
+
* - a permanently dead session (401/403/411/440) being reconnected forever, which is the
|
|
14
|
+
* documented path to a ban (this is what DEAD_REASONS stops);
|
|
15
|
+
* - unbounded parallel connects, which exhausts FDs and TLS handshakes on boot.
|
|
16
|
+
*
|
|
17
|
+
* Events are deliberately never named 'error': EventEmitter throws on an unhandled 'error', and one
|
|
18
|
+
* dead session must not take the process down.
|
|
19
|
+
*
|
|
20
|
+
* ponytail: supervision only. Every socket still shares one event loop, so this bounds reconnect
|
|
21
|
+
* storms and socket leaks but not CPU-bound crypto; past a few hundred accounts, shard by process.
|
|
22
|
+
*/
|
|
23
|
+
const DEAD_REASONS = new Set([
|
|
24
|
+
DisconnectReason.loggedOut,
|
|
25
|
+
DisconnectReason.forbidden,
|
|
26
|
+
DisconnectReason.multideviceMismatch,
|
|
27
|
+
DisconnectReason.badSession,
|
|
28
|
+
DisconnectReason.connectionReplaced
|
|
29
|
+
]);
|
|
30
|
+
export class ConnectionManager extends EventEmitter {
|
|
31
|
+
#create;
|
|
32
|
+
#sessions = new Map();
|
|
33
|
+
#maxConcurrent;
|
|
34
|
+
#active = 0;
|
|
35
|
+
#waiters = [];
|
|
36
|
+
#reconnect;
|
|
37
|
+
#baseDelayMs;
|
|
38
|
+
#maxDelayMs;
|
|
39
|
+
#maxAttempts;
|
|
40
|
+
#staggerMs;
|
|
41
|
+
#logger;
|
|
42
|
+
#started = false;
|
|
43
|
+
constructor({ create, sessions = [], maxConcurrent = 8, reconnect = true, baseDelayMs = 2000, maxDelayMs = 60000, maxAttempts = Infinity, staggerMs = 250, logger = console }) {
|
|
44
|
+
super();
|
|
45
|
+
if (typeof create !== 'function') {
|
|
46
|
+
throw new TypeError('ConnectionManager needs a create(session) function');
|
|
47
|
+
}
|
|
48
|
+
for (const session of sessions) {
|
|
49
|
+
if (!session?.id) {
|
|
50
|
+
throw new TypeError('every session needs an id');
|
|
51
|
+
}
|
|
52
|
+
if (this.#sessions.has(session.id)) {
|
|
53
|
+
throw new TypeError(`duplicate session id: ${session.id}`);
|
|
54
|
+
}
|
|
55
|
+
this.#sessions.set(session.id, {
|
|
56
|
+
id: session.id,
|
|
57
|
+
session,
|
|
58
|
+
sock: null,
|
|
59
|
+
state: 'idle',
|
|
60
|
+
attempts: 0,
|
|
61
|
+
generation: 0,
|
|
62
|
+
timer: null,
|
|
63
|
+
stopped: false,
|
|
64
|
+
lastError: null
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
this.#create = create;
|
|
68
|
+
this.#maxConcurrent = Math.max(1, maxConcurrent);
|
|
69
|
+
this.#reconnect = reconnect;
|
|
70
|
+
this.#baseDelayMs = baseDelayMs;
|
|
71
|
+
this.#maxDelayMs = maxDelayMs;
|
|
72
|
+
this.#maxAttempts = maxAttempts;
|
|
73
|
+
this.#staggerMs = staggerMs;
|
|
74
|
+
this.#logger = logger;
|
|
75
|
+
}
|
|
76
|
+
/** Connect every session, staggered so boot does not fire them all in one tick. */
|
|
77
|
+
start() {
|
|
78
|
+
if (this.#started) {
|
|
79
|
+
return this.stats();
|
|
80
|
+
}
|
|
81
|
+
this.#started = true;
|
|
82
|
+
let index = 0;
|
|
83
|
+
for (const rec of this.#sessions.values()) {
|
|
84
|
+
const delayMs = this.#staggerMs * index++;
|
|
85
|
+
if (!delayMs) {
|
|
86
|
+
void this.#connect(rec);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
rec.timer = setTimeout(() => {
|
|
90
|
+
rec.timer = null;
|
|
91
|
+
void this.#connect(rec);
|
|
92
|
+
}, delayMs);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return this.stats();
|
|
96
|
+
}
|
|
97
|
+
get(id) {
|
|
98
|
+
return this.#sessions.get(id)?.sock ?? null;
|
|
99
|
+
}
|
|
100
|
+
/** Stop one session for good. A close event that arrives afterwards is ignored. */
|
|
101
|
+
stop(id) {
|
|
102
|
+
const rec = this.#sessions.get(id);
|
|
103
|
+
if (!rec) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
this.#stop(rec);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** Stop everything and wait for each socket's teardown. */
|
|
110
|
+
async close() {
|
|
111
|
+
const pending = [];
|
|
112
|
+
for (const rec of this.#sessions.values()) {
|
|
113
|
+
const sock = this.#stop(rec);
|
|
114
|
+
if (sock) {
|
|
115
|
+
pending.push(Promise.resolve(sock.end?.(undefined)).catch(() => { }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await Promise.all(pending);
|
|
119
|
+
this.#started = false;
|
|
120
|
+
this.removeAllListeners();
|
|
121
|
+
}
|
|
122
|
+
stats() {
|
|
123
|
+
const counts = { total: 0, idle: 0, connecting: 0, open: 0, reconnecting: 0, closed: 0, failed: 0 };
|
|
124
|
+
for (const rec of this.#sessions.values()) {
|
|
125
|
+
counts.total += 1;
|
|
126
|
+
counts[rec.state] += 1;
|
|
127
|
+
}
|
|
128
|
+
counts.activeConnects = this.#active;
|
|
129
|
+
counts.queuedConnects = this.#waiters.length;
|
|
130
|
+
return counts;
|
|
131
|
+
}
|
|
132
|
+
#stop(rec) {
|
|
133
|
+
rec.stopped = true;
|
|
134
|
+
rec.generation += 1;
|
|
135
|
+
if (rec.timer) {
|
|
136
|
+
clearTimeout(rec.timer);
|
|
137
|
+
rec.timer = null;
|
|
138
|
+
}
|
|
139
|
+
const sock = rec.sock;
|
|
140
|
+
rec.sock = null;
|
|
141
|
+
rec.state = 'closed';
|
|
142
|
+
return sock;
|
|
143
|
+
}
|
|
144
|
+
async #connect(rec) {
|
|
145
|
+
if (rec.stopped) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (rec.timer) {
|
|
149
|
+
clearTimeout(rec.timer);
|
|
150
|
+
rec.timer = null;
|
|
151
|
+
}
|
|
152
|
+
const generation = ++rec.generation;
|
|
153
|
+
rec.state = 'connecting';
|
|
154
|
+
await this.#acquire();
|
|
155
|
+
let sock;
|
|
156
|
+
try {
|
|
157
|
+
sock = await this.#create(rec.session);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
this.#release();
|
|
161
|
+
rec.lastError = err;
|
|
162
|
+
this.emit('session-error', { id: rec.id, error: err });
|
|
163
|
+
this.#scheduleReconnect(rec, err);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.#release();
|
|
167
|
+
// stop() or a newer connect won the race while this one was being created
|
|
168
|
+
if (rec.stopped || generation !== rec.generation) {
|
|
169
|
+
void sock?.end?.(undefined);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
rec.sock = sock;
|
|
173
|
+
try {
|
|
174
|
+
sock.ev.on('connection.update', update => this.#onUpdate(rec, sock, update));
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
rec.lastError = err;
|
|
178
|
+
this.emit('session-error', { id: rec.id, error: err });
|
|
179
|
+
this.#scheduleReconnect(rec, err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
#onUpdate(rec, sock, update) {
|
|
183
|
+
// a socket that was already replaced must not drive this session any more
|
|
184
|
+
if (rec.sock !== sock) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const { connection, lastDisconnect, qr } = update;
|
|
188
|
+
if (qr) {
|
|
189
|
+
this.emit('qr', { id: rec.id, qr });
|
|
190
|
+
}
|
|
191
|
+
if (connection === 'open') {
|
|
192
|
+
rec.state = 'open';
|
|
193
|
+
rec.attempts = 0;
|
|
194
|
+
rec.lastError = null;
|
|
195
|
+
this.emit('open', { id: rec.id, sock });
|
|
196
|
+
}
|
|
197
|
+
else if (connection === 'close') {
|
|
198
|
+
const error = lastDisconnect?.error;
|
|
199
|
+
rec.sock = null;
|
|
200
|
+
rec.lastError = error;
|
|
201
|
+
this.emit('close', { id: rec.id, error, statusCode: error?.output?.statusCode });
|
|
202
|
+
this.#scheduleReconnect(rec, error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
#scheduleReconnect(rec, error) {
|
|
206
|
+
if (!this.#reconnect || rec.stopped) {
|
|
207
|
+
rec.state = 'closed';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const statusCode = error?.output?.statusCode;
|
|
211
|
+
if (DEAD_REASONS.has(statusCode) || rec.attempts >= this.#maxAttempts) {
|
|
212
|
+
rec.state = 'failed';
|
|
213
|
+
this.#logger?.warn?.({ id: rec.id, statusCode, attempts: rec.attempts }, 'session will not be reconnected');
|
|
214
|
+
this.emit('dead', { id: rec.id, error, statusCode });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// the server asks for a restart on 515; backing off there only delays the reconnect
|
|
218
|
+
const immediate = statusCode === DisconnectReason.restartRequired;
|
|
219
|
+
const backoff = Math.min(this.#maxDelayMs, this.#baseDelayMs * 2 ** rec.attempts);
|
|
220
|
+
const delayMs = immediate ? 0 : Math.round(backoff * (0.75 + Math.random() * 0.5));
|
|
221
|
+
rec.attempts += 1;
|
|
222
|
+
rec.state = 'reconnecting';
|
|
223
|
+
this.emit('reconnect', { id: rec.id, attempt: rec.attempts, delayMs, statusCode });
|
|
224
|
+
rec.timer = setTimeout(() => {
|
|
225
|
+
rec.timer = null;
|
|
226
|
+
void this.#connect(rec);
|
|
227
|
+
}, delayMs);
|
|
228
|
+
}
|
|
229
|
+
#acquire() {
|
|
230
|
+
if (this.#active < this.#maxConcurrent) {
|
|
231
|
+
this.#active += 1;
|
|
232
|
+
return Promise.resolve();
|
|
233
|
+
}
|
|
234
|
+
return new Promise(resolve => this.#waiters.push(resolve));
|
|
235
|
+
}
|
|
236
|
+
#release() {
|
|
237
|
+
const next = this.#waiters.shift();
|
|
238
|
+
if (next) {
|
|
239
|
+
// hand the slot over without touching the counter
|
|
240
|
+
next();
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.#active -= 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export const makeConnectionManager = (options) => new ConnectionManager(options);
|
|
248
|
+
/**
|
|
249
|
+
* One TLS agent shared by every socket in the fleet. `ws` opens one TCP connection per socket, so
|
|
250
|
+
* this does not multiplex them, but it does keep the agent, its DNS cache and its free-socket pool
|
|
251
|
+
* shared instead of one per account.
|
|
252
|
+
*/
|
|
253
|
+
export const makeWssAgent = ({ maxSockets = 256, keepAlive = true, keepAliveMsecs = 30000 } = {}) => new Agent({
|
|
254
|
+
keepAlive,
|
|
255
|
+
keepAliveMsecs,
|
|
256
|
+
maxSockets,
|
|
257
|
+
maxFreeSockets: Math.min(32, maxSockets)
|
|
258
|
+
});
|
|
@@ -400,7 +400,8 @@ eventData, logger) {
|
|
|
400
400
|
data.contactUpserts[contact.id] = upsert;
|
|
401
401
|
}
|
|
402
402
|
if (data.contactUpdates[contact.id]) {
|
|
403
|
-
upsert
|
|
403
|
+
// merge into the stored upsert itself; the old code merged into a temp and lost the update
|
|
404
|
+
Object.assign(upsert, trimUndefined(data.contactUpdates[contact.id]), trimUndefined(contact));
|
|
404
405
|
delete data.contactUpdates[contact.id];
|
|
405
406
|
}
|
|
406
407
|
}
|
|
@@ -525,10 +526,8 @@ eventData, logger) {
|
|
|
525
526
|
const groupUpdates = eventData;
|
|
526
527
|
for (const update of groupUpdates) {
|
|
527
528
|
const id = update.id;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
data.groupUpdates[id] = Object.assign(groupUpdate, update);
|
|
531
|
-
}
|
|
529
|
+
// merge every update in the window, not just the first
|
|
530
|
+
data.groupUpdates[id] = Object.assign(data.groupUpdates[id] || {}, update);
|
|
532
531
|
}
|
|
533
532
|
break;
|
|
534
533
|
default:
|
package/lib/Utils/generics.js
CHANGED
|
@@ -263,17 +263,32 @@ export const getStatusFromReceiptType = (type) => {
|
|
|
263
263
|
}
|
|
264
264
|
return status;
|
|
265
265
|
};
|
|
266
|
-
const CODE_MAP = {
|
|
267
|
-
conflict: DisconnectReason.connectionReplaced
|
|
268
|
-
};
|
|
269
266
|
/**
|
|
270
|
-
* Stream errors generally provide a reason, map that to a baileys DisconnectReason
|
|
271
|
-
*
|
|
267
|
+
* Stream errors generally provide a reason, map that to a baileys DisconnectReason.
|
|
268
|
+
* Mirrors zapo-js (src/transport/stream/parse.ts): only conflict means the session is gone;
|
|
269
|
+
* ack / xml-not-well-formed / unknown reasons are transient and must stay reconnectable.
|
|
270
|
+
* The old fallback was badSession (500), which made bots wipe healthy sessions.
|
|
272
271
|
*/
|
|
273
272
|
export const getErrorCodeFromStreamError = (node) => {
|
|
274
273
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
275
274
|
let reason = reasonNode?.tag || 'unknown';
|
|
276
|
-
|
|
275
|
+
let statusCode;
|
|
276
|
+
if (reason === 'conflict') {
|
|
277
|
+
// type="replaced": another client took over; any other type (device_removed): unlinked
|
|
278
|
+
statusCode = reasonNode.attrs?.type === 'replaced' || !reasonNode.attrs?.type
|
|
279
|
+
? DisconnectReason.connectionReplaced
|
|
280
|
+
: DisconnectReason.loggedOut;
|
|
281
|
+
}
|
|
282
|
+
else if (node.attrs.code) {
|
|
283
|
+
statusCode = +node.attrs.code;
|
|
284
|
+
// 516 = server-forced logout
|
|
285
|
+
if (statusCode === 516) {
|
|
286
|
+
statusCode = DisconnectReason.loggedOut;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
statusCode = DisconnectReason.connectionClosed;
|
|
291
|
+
}
|
|
277
292
|
if (statusCode === DisconnectReason.restartRequired) {
|
|
278
293
|
reason = 'restart required';
|
|
279
294
|
}
|
package/lib/Utils/ignore-key.js
CHANGED
|
@@ -101,7 +101,8 @@ export const buildIgnoreContext = (node, meId, meLid) => {
|
|
|
101
101
|
const fromMe = !!author && ((!!meId && areJidsSameUser(author, meId)) || (!!meLid && areJidsSameUser(author, meLid)));
|
|
102
102
|
return {
|
|
103
103
|
kind: node.tag,
|
|
104
|
-
|
|
104
|
+
// a 1:1 message sent from another of our devices arrives from us, with the chat in `recipient`
|
|
105
|
+
remoteJid: stripDevice(fromMe && attrs.recipient ? attrs.recipient : attrs.from),
|
|
105
106
|
fromMe,
|
|
106
107
|
id: attrs.id,
|
|
107
108
|
participant: stripDevice(attrs.participant || attrs.participant_pn || attrs.participant_lid)
|
|
@@ -121,7 +122,8 @@ export const matchesIgnoreKey = (entry, ctx, node) => {
|
|
|
121
122
|
return false;
|
|
122
123
|
}
|
|
123
124
|
const attrs = node.attrs || {};
|
|
124
|
-
|
|
125
|
+
const remoteAttrs = ctx.fromMe && attrs.recipient ? ['recipient', ...REMOTE_JID_ATTRS] : REMOTE_JID_ATTRS;
|
|
126
|
+
if (entry.remoteJid && !entry.remoteJid.some(wanted => remoteAttrs.some(attr => sameJid(attrs[attr], wanted)))) {
|
|
125
127
|
return false;
|
|
126
128
|
}
|
|
127
129
|
if (entry.participant && !PARTICIPANT_ATTRS.some(attr => sameJid(attrs[attr], entry.participant))) {
|
package/lib/Utils/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from "./identity-change-handler.js";
|
|
|
24
24
|
export * from "./stanza-ack.js";
|
|
25
25
|
export * from "./ignore-key.js";
|
|
26
26
|
export * from "./anchor-guard.js";
|
|
27
|
+
export * from "./connection-manager.js";
|
|
27
28
|
export * from "./a2ui.js";
|
|
28
29
|
export * from "./meta-ai-msmsg.js";
|
|
29
30
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/Utils/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export * from './event-buffer.js';
|
|
|
18
18
|
export * from './process-message.js';
|
|
19
19
|
export * from './rich-message-utils.js';
|
|
20
20
|
export * from './message-retry-manager.js';
|
|
21
|
+
export * from './connection-manager.js';
|
|
21
22
|
export * from './browser-utils.js';
|
|
22
23
|
export * from './companion-reg-client-utils.js';
|
|
23
24
|
export * from './identity-change-handler.js';
|
|
@@ -57,6 +57,8 @@ export interface MediaEncryptionOptions {
|
|
|
57
57
|
};
|
|
58
58
|
saveOriginalFileIfRequired?: boolean;
|
|
59
59
|
opts?: MediaFetchOptions;
|
|
60
|
+
/** Reuse an existing 32-byte media key instead of generating one (e.g. sticker-pack thumbnails). */
|
|
61
|
+
mediaKey?: Uint8Array;
|
|
60
62
|
}
|
|
61
63
|
export interface MediaDownloadOptions {
|
|
62
64
|
startByte?: number;
|
|
@@ -481,10 +481,11 @@ class SidecarAccumulator {
|
|
|
481
481
|
this.windowOffset = SIDECAR_IV_SIZE;
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
|
-
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
484
|
+
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, mediaKey: givenMediaKey } = {}) => {
|
|
485
485
|
const { stream, type } = await getStream(media, opts);
|
|
486
486
|
logger?.debug('fetched media stream');
|
|
487
|
-
|
|
487
|
+
// sticker-pack thumbnails must reuse the pack's key, the message carries only that one
|
|
488
|
+
const mediaKey = givenMediaKey ?? Crypto.randomBytes(32);
|
|
488
489
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
489
490
|
const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
|
|
490
491
|
const encFileWriteStream = createWriteStream(encFilePath);
|
package/lib/Utils/messages.js
CHANGED
|
@@ -105,7 +105,8 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
105
105
|
if (mediaBuff) {
|
|
106
106
|
logger?.debug({ cacheableKey }, 'got media cache hit');
|
|
107
107
|
const obj = proto.Message.decode(mediaBuff);
|
|
108
|
-
|
|
108
|
+
// ptv entries are cached after videoMessage was renamed to ptvMessage
|
|
109
|
+
const key = obj.ptvMessage ? 'ptvMessage' : `${mediaType}Message`;
|
|
109
110
|
Object.assign(obj[key], { ...uploadData, media: undefined });
|
|
110
111
|
return obj;
|
|
111
112
|
}
|
|
@@ -114,7 +114,10 @@ export const isRealMessage = (message) => {
|
|
|
114
114
|
hasSomeContent &&
|
|
115
115
|
!normalizedContent?.protocolMessage &&
|
|
116
116
|
!normalizedContent?.reactionMessage &&
|
|
117
|
-
!normalizedContent?.pollUpdateMessage
|
|
117
|
+
!normalizedContent?.pollUpdateMessage &&
|
|
118
|
+
// edit envelopes and event responses are not new chat messages (unread/timestamp bump)
|
|
119
|
+
!normalizedContent?.secretEncryptedMessage &&
|
|
120
|
+
!normalizedContent?.encEventResponseMessage);
|
|
118
121
|
};
|
|
119
122
|
export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
|
|
120
123
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@violetix/baileys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Enhanced Baileys v7 with fixes for newsletter media uploads, plus support for interactive messages, albums, and additional message types.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|