@queenanya/baileys 9.2.1 → 9.4.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 +349 -1171
- package/WAProto/fix-imports.js +74 -18
- package/WAProto/index.js +201 -160
- package/engine-requirements.js +7 -7
- package/lib/Defaults/index.d.ts +19 -0
- package/lib/Defaults/index.d.ts.map +1 -1
- package/lib/Defaults/index.js +32 -6
- package/lib/Defaults/index.js.map +1 -1
- package/lib/Signal/libsignal.d.ts.map +1 -1
- package/lib/Signal/libsignal.js +61 -2
- package/lib/Signal/libsignal.js.map +1 -1
- package/lib/Signal/lid-mapping.d.ts +5 -9
- package/lib/Signal/lid-mapping.d.ts.map +1 -1
- package/lib/Signal/lid-mapping.js +170 -70
- package/lib/Signal/lid-mapping.js.map +1 -1
- package/lib/Socket/Client/websocket.d.ts +1 -1
- package/lib/Socket/Client/websocket.d.ts.map +1 -1
- package/lib/Socket/Client/websocket.js +5 -1
- package/lib/Socket/Client/websocket.js.map +1 -1
- package/lib/Socket/business.d.ts +125 -5
- package/lib/Socket/business.d.ts.map +1 -1
- package/lib/Socket/business.js +11 -8
- package/lib/Socket/business.js.map +1 -1
- package/lib/Socket/chats.d.ts +22 -3
- package/lib/Socket/chats.d.ts.map +1 -1
- package/lib/Socket/chats.js +277 -58
- package/lib/Socket/chats.js.map +1 -1
- package/lib/Socket/communities.d.ts +125 -5
- package/lib/Socket/communities.d.ts.map +1 -1
- package/lib/Socket/groups.d.ts +19 -3
- package/lib/Socket/groups.d.ts.map +1 -1
- package/lib/Socket/groups.js +7 -1
- package/lib/Socket/groups.js.map +1 -1
- package/lib/Socket/index.d.ts +125 -5
- package/lib/Socket/index.d.ts.map +1 -1
- package/lib/Socket/index.js +0 -6
- package/lib/Socket/index.js.map +1 -1
- package/lib/Socket/messages-recv.d.ts +126 -6
- package/lib/Socket/messages-recv.d.ts.map +1 -1
- package/lib/Socket/messages-recv.js +771 -177
- package/lib/Socket/messages-recv.js.map +1 -1
- package/lib/Socket/messages-send.d.ts +129 -7
- package/lib/Socket/messages-send.d.ts.map +1 -1
- package/lib/Socket/messages-send.js +430 -119
- package/lib/Socket/messages-send.js.map +1 -1
- package/lib/Socket/newsletter.d.ts +20 -5
- package/lib/Socket/newsletter.d.ts.map +1 -1
- package/lib/Socket/newsletter.js +2 -47
- package/lib/Socket/newsletter.js.map +1 -1
- package/lib/Socket/socket.d.ts +3 -1
- package/lib/Socket/socket.d.ts.map +1 -1
- package/lib/Socket/socket.js +151 -29
- package/lib/Socket/socket.js.map +1 -1
- package/lib/Types/Auth.d.ts +2 -0
- package/lib/Types/Auth.d.ts.map +1 -1
- package/lib/Types/Call.d.ts +10 -1
- package/lib/Types/Call.d.ts.map +1 -1
- package/lib/Types/Contact.d.ts +2 -0
- package/lib/Types/Contact.d.ts.map +1 -1
- package/lib/Types/Events.d.ts +60 -6
- package/lib/Types/Events.d.ts.map +1 -1
- package/lib/Types/GroupMetadata.d.ts +4 -0
- package/lib/Types/GroupMetadata.d.ts.map +1 -1
- package/lib/Types/Message.d.ts +530 -16
- package/lib/Types/Message.d.ts.map +1 -1
- package/lib/Types/Message.js.map +1 -1
- package/lib/Types/Newsletter.d.ts +32 -45
- package/lib/Types/Newsletter.d.ts.map +1 -1
- package/lib/Types/Newsletter.js +25 -23
- package/lib/Types/Newsletter.js.map +1 -1
- package/lib/Types/State.d.ts +54 -0
- package/lib/Types/State.d.ts.map +1 -1
- package/lib/Types/State.js +42 -0
- package/lib/Types/State.js.map +1 -1
- package/lib/Types/index.d.ts +9 -0
- package/lib/Types/index.d.ts.map +1 -1
- package/lib/Types/index.js.map +1 -1
- package/lib/Utils/auth-utils.d.ts.map +1 -1
- package/lib/Utils/auth-utils.js +53 -20
- package/lib/Utils/auth-utils.js.map +1 -1
- package/lib/Utils/browser-utils.d.ts +13 -0
- package/lib/Utils/browser-utils.d.ts.map +1 -1
- package/lib/Utils/browser-utils.js +90 -10
- package/lib/Utils/browser-utils.js.map +1 -1
- package/lib/Utils/chat-utils.d.ts +30 -0
- package/lib/Utils/chat-utils.d.ts.map +1 -1
- package/lib/Utils/chat-utils.js +134 -59
- package/lib/Utils/chat-utils.js.map +1 -1
- package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
- package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
- package/lib/Utils/companion-reg-client-utils.js +34 -0
- package/lib/Utils/companion-reg-client-utils.js.map +1 -0
- package/lib/Utils/crypto.d.ts +4 -8
- package/lib/Utils/crypto.d.ts.map +1 -1
- package/lib/Utils/crypto.js +2 -26
- package/lib/Utils/crypto.js.map +1 -1
- package/lib/Utils/decode-wa-message.d.ts +12 -0
- package/lib/Utils/decode-wa-message.d.ts.map +1 -1
- package/lib/Utils/decode-wa-message.js +16 -0
- package/lib/Utils/decode-wa-message.js.map +1 -1
- package/lib/Utils/event-buffer.d.ts.map +1 -1
- package/lib/Utils/event-buffer.js +43 -8
- package/lib/Utils/event-buffer.js.map +1 -1
- package/lib/Utils/generics.d.ts +3 -1
- package/lib/Utils/generics.d.ts.map +1 -1
- package/lib/Utils/generics.js +17 -4
- package/lib/Utils/generics.js.map +1 -1
- package/lib/Utils/history.d.ts +8 -3
- package/lib/Utils/history.d.ts.map +1 -1
- package/lib/Utils/history.js +60 -16
- package/lib/Utils/history.js.map +1 -1
- package/lib/Utils/identity-change-handler.d.ts +44 -0
- package/lib/Utils/identity-change-handler.d.ts.map +1 -0
- package/lib/Utils/identity-change-handler.js +50 -0
- package/lib/Utils/identity-change-handler.js.map +1 -0
- package/lib/Utils/index.d.ts +6 -0
- package/lib/Utils/index.d.ts.map +1 -1
- package/lib/Utils/index.js +6 -0
- package/lib/Utils/index.js.map +1 -1
- package/lib/Utils/interactive-message.d.ts +201 -0
- package/lib/Utils/interactive-message.d.ts.map +1 -0
- package/lib/Utils/interactive-message.js +256 -0
- package/lib/Utils/interactive-message.js.map +1 -0
- package/lib/Utils/lt-hash.d.ts +7 -12
- package/lib/Utils/lt-hash.d.ts.map +1 -1
- package/lib/Utils/lt-hash.js +2 -42
- package/lib/Utils/lt-hash.js.map +1 -1
- package/lib/Utils/make-mutex.d.ts +1 -0
- package/lib/Utils/make-mutex.d.ts.map +1 -1
- package/lib/Utils/make-mutex.js +20 -27
- package/lib/Utils/make-mutex.js.map +1 -1
- package/lib/Utils/message-composer.d.ts +5 -0
- package/lib/Utils/message-composer.d.ts.map +1 -0
- package/lib/Utils/message-composer.js +5 -0
- package/lib/Utils/message-composer.js.map +1 -0
- package/lib/Utils/message-retry-manager.d.ts +30 -2
- package/lib/Utils/message-retry-manager.d.ts.map +1 -1
- package/lib/Utils/message-retry-manager.js +58 -5
- package/lib/Utils/message-retry-manager.js.map +1 -1
- package/lib/Utils/messages-media.d.ts +35 -5
- package/lib/Utils/messages-media.d.ts.map +1 -1
- package/lib/Utils/messages-media.js +171 -51
- package/lib/Utils/messages-media.js.map +1 -1
- package/lib/Utils/messages.d.ts +2 -0
- package/lib/Utils/messages.d.ts.map +1 -1
- package/lib/Utils/messages.js +475 -35
- package/lib/Utils/messages.js.map +1 -1
- package/lib/Utils/noise-handler.d.ts +4 -4
- package/lib/Utils/noise-handler.d.ts.map +1 -1
- package/lib/Utils/noise-handler.js +139 -85
- package/lib/Utils/noise-handler.js.map +1 -1
- package/lib/Utils/offline-node-processor.d.ts +17 -0
- package/lib/Utils/offline-node-processor.d.ts.map +1 -0
- package/lib/Utils/offline-node-processor.js +40 -0
- package/lib/Utils/offline-node-processor.js.map +1 -0
- package/lib/Utils/process-message.d.ts.map +1 -1
- package/lib/Utils/process-message.js +115 -16
- package/lib/Utils/process-message.js.map +1 -1
- package/lib/Utils/reporting-utils.d.ts +11 -0
- package/lib/Utils/reporting-utils.d.ts.map +1 -0
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/reporting-utils.js.map +1 -0
- package/lib/Utils/stanza-ack.d.ts +11 -0
- package/lib/Utils/stanza-ack.d.ts.map +1 -0
- package/lib/Utils/stanza-ack.js +38 -0
- package/lib/Utils/stanza-ack.js.map +1 -0
- package/lib/Utils/sync-action-utils.d.ts +19 -0
- package/lib/Utils/sync-action-utils.d.ts.map +1 -0
- package/lib/Utils/sync-action-utils.js +49 -0
- package/lib/Utils/sync-action-utils.js.map +1 -0
- package/lib/Utils/tc-token-utils.d.ts +37 -0
- package/lib/Utils/tc-token-utils.d.ts.map +1 -0
- package/lib/Utils/tc-token-utils.js +163 -0
- package/lib/Utils/tc-token-utils.js.map +1 -0
- package/lib/Utils/use-mongo-file-auth-state.d.ts +16 -0
- package/lib/Utils/use-mongo-file-auth-state.d.ts.map +1 -0
- package/lib/Utils/use-mongo-file-auth-state.js +60 -0
- package/lib/Utils/use-mongo-file-auth-state.js.map +1 -0
- package/lib/Utils/use-multi-file-auth-state.js +1 -1
- package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
- package/lib/Utils/use-single-file-auth-state.d.ts.map +1 -1
- package/lib/Utils/use-single-file-auth-state.js.map +1 -1
- package/lib/Utils/validate-connection.d.ts.map +1 -1
- package/lib/Utils/validate-connection.js +11 -1
- package/lib/Utils/validate-connection.js.map +1 -1
- package/lib/WABinary/decode.d.ts.map +1 -1
- package/lib/WABinary/decode.js +24 -0
- package/lib/WABinary/decode.js.map +1 -1
- package/lib/WABinary/encode.js +5 -1
- package/lib/WABinary/encode.js.map +1 -1
- package/lib/WABinary/generic-utils.d.ts +10 -1
- package/lib/WABinary/generic-utils.d.ts.map +1 -1
- package/lib/WABinary/generic-utils.js +42 -8
- package/lib/WABinary/generic-utils.js.map +1 -1
- package/lib/WABinary/jid-utils.js.map +1 -1
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -1
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -1
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
- package/lib/WAUSync/Protocols/index.d.ts +1 -0
- package/lib/WAUSync/Protocols/index.d.ts.map +1 -1
- package/lib/WAUSync/Protocols/index.js +1 -0
- package/lib/WAUSync/Protocols/index.js.map +1 -1
- package/lib/WAUSync/USyncQuery.d.ts +1 -0
- package/lib/WAUSync/USyncQuery.d.ts.map +1 -1
- package/lib/WAUSync/USyncQuery.js +6 -2
- package/lib/WAUSync/USyncQuery.js.map +1 -1
- package/lib/WAUSync/USyncUser.d.ts +4 -0
- package/lib/WAUSync/USyncUser.d.ts.map +1 -1
- package/lib/WAUSync/USyncUser.js +8 -0
- package/lib/WAUSync/USyncUser.js.map +1 -1
- package/lib/addons/anti-delete.d.ts +72 -0
- package/lib/addons/anti-delete.d.ts.map +1 -0
- package/lib/addons/anti-delete.js +165 -0
- package/lib/addons/anti-delete.js.map +1 -0
- package/lib/addons/auto-reply.d.ts +67 -0
- package/lib/addons/auto-reply.d.ts.map +1 -0
- package/lib/addons/auto-reply.js +145 -0
- package/lib/addons/auto-reply.js.map +1 -0
- package/lib/addons/browser-presets.d.ts +16 -0
- package/lib/addons/browser-presets.d.ts.map +1 -0
- package/lib/addons/browser-presets.js +24 -0
- package/lib/addons/browser-presets.js.map +1 -0
- package/lib/addons/button-sender.d.ts +260 -0
- package/lib/addons/button-sender.d.ts.map +1 -0
- package/lib/addons/button-sender.js +771 -0
- package/lib/addons/button-sender.js.map +1 -0
- package/lib/addons/call-handler.d.ts +79 -0
- package/lib/addons/call-handler.d.ts.map +1 -0
- package/lib/addons/call-handler.js +342 -0
- package/lib/addons/call-handler.js.map +1 -0
- package/lib/addons/from-chats.d.ts +30 -0
- package/lib/addons/from-chats.d.ts.map +1 -0
- package/lib/addons/from-chats.js +38 -0
- package/lib/addons/from-chats.js.map +1 -0
- package/lib/addons/from-messages-recv.d.ts +59 -0
- package/lib/addons/from-messages-recv.d.ts.map +1 -0
- package/lib/addons/from-messages-recv.js +326 -0
- package/lib/addons/from-messages-recv.js.map +1 -0
- package/lib/addons/from-messages-send.d.ts +50 -0
- package/lib/addons/from-messages-send.d.ts.map +1 -0
- package/lib/addons/from-messages-send.js +148 -0
- package/lib/addons/from-messages-send.js.map +1 -0
- package/lib/addons/from-messages.d.ts +52 -0
- package/lib/addons/from-messages.d.ts.map +1 -0
- package/lib/addons/from-messages.js +304 -0
- package/lib/addons/from-messages.js.map +1 -0
- package/lib/addons/index.d.ts +67 -0
- package/lib/addons/index.d.ts.map +1 -0
- package/lib/addons/index.js +86 -0
- package/lib/addons/index.js.map +1 -0
- package/lib/addons/interactive-message.d.ts +201 -0
- package/lib/addons/interactive-message.d.ts.map +1 -0
- package/lib/addons/interactive-message.js +256 -0
- package/lib/addons/interactive-message.js.map +1 -0
- package/lib/addons/jid-plot.d.ts +49 -0
- package/lib/addons/jid-plot.d.ts.map +1 -0
- package/lib/addons/jid-plot.js +84 -0
- package/lib/addons/jid-plot.js.map +1 -0
- package/lib/addons/jid-plotting.d.ts +54 -0
- package/lib/addons/jid-plotting.d.ts.map +1 -0
- package/lib/addons/jid-plotting.js +150 -0
- package/lib/addons/jid-plotting.js.map +1 -0
- package/lib/addons/lid-support.d.ts +41 -0
- package/lib/addons/lid-support.d.ts.map +1 -0
- package/lib/addons/lid-support.js +42 -0
- package/lib/addons/lid-support.js.map +1 -0
- package/lib/addons/message-composer.d.ts +142 -0
- package/lib/addons/message-composer.d.ts.map +1 -0
- package/lib/addons/message-composer.js +377 -0
- package/lib/addons/message-composer.js.map +1 -0
- package/lib/addons/message-scheduler.d.ts +77 -0
- package/lib/addons/message-scheduler.d.ts.map +1 -0
- package/lib/addons/message-scheduler.js +108 -0
- package/lib/addons/message-scheduler.js.map +1 -0
- package/lib/addons/message-search.d.ts +51 -0
- package/lib/addons/message-search.d.ts.map +1 -0
- package/lib/addons/message-search.js +171 -0
- package/lib/addons/message-search.js.map +1 -0
- package/lib/addons/message-utils.d.ts +88 -0
- package/lib/addons/message-utils.d.ts.map +1 -0
- package/lib/addons/message-utils.js +292 -0
- package/lib/addons/message-utils.js.map +1 -0
- package/lib/addons/outgoing-calls.d.ts +64 -0
- package/lib/addons/outgoing-calls.d.ts.map +1 -0
- package/lib/addons/outgoing-calls.js +139 -0
- package/lib/addons/outgoing-calls.js.map +1 -0
- package/lib/addons/pairing-fix.d.ts +31 -0
- package/lib/addons/pairing-fix.d.ts.map +1 -0
- package/lib/addons/pairing-fix.js +74 -0
- package/lib/addons/pairing-fix.js.map +1 -0
- package/lib/addons/past-participants.d.ts +42 -0
- package/lib/addons/past-participants.d.ts.map +1 -0
- package/lib/addons/past-participants.js +41 -0
- package/lib/addons/past-participants.js.map +1 -0
- package/lib/addons/rich-response.d.ts +111 -0
- package/lib/addons/rich-response.d.ts.map +1 -0
- package/lib/addons/rich-response.js +152 -0
- package/lib/addons/rich-response.js.map +1 -0
- package/lib/addons/scheduling.d.ts +41 -0
- package/lib/addons/scheduling.d.ts.map +1 -0
- package/lib/addons/scheduling.js +110 -0
- package/lib/addons/scheduling.js.map +1 -0
- package/lib/addons/status-posting.d.ts +177 -0
- package/lib/addons/status-posting.d.ts.map +1 -0
- package/lib/addons/status-posting.js +240 -0
- package/lib/addons/status-posting.js.map +1 -0
- package/lib/addons/stickerpack.d.ts +37 -0
- package/lib/addons/stickerpack.d.ts.map +1 -0
- package/lib/addons/stickerpack.js +39 -0
- package/lib/addons/stickerpack.js.map +1 -0
- package/lib/addons/templates.d.ts +72 -0
- package/lib/addons/templates.d.ts.map +1 -0
- package/lib/addons/templates.js +145 -0
- package/lib/addons/templates.js.map +1 -0
- package/lib/addons/vcard.d.ts +59 -0
- package/lib/addons/vcard.d.ts.map +1 -0
- package/lib/addons/vcard.js +88 -0
- package/lib/addons/vcard.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +6 -3
|
@@ -3,17 +3,19 @@ import { Boom } from '@hapi/boom';
|
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import Long from 'long';
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
|
-
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
|
|
7
|
-
import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
-
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
6
|
+
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
|
|
7
|
+
import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, 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';
|
|
9
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
|
-
import {
|
|
10
|
+
import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
|
|
11
|
+
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
12
|
import { extractGroupMetadata } from './groups.js';
|
|
12
13
|
import { makeMessagesSocket } from './messages-send.js';
|
|
13
14
|
export const makeMessagesRecvSocket = (config) => {
|
|
14
15
|
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
15
16
|
const sock = makeMessagesSocket(config);
|
|
16
|
-
const { ev, authState, ws,
|
|
17
|
+
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, getUSyncDevices, createParticipantNodes, messageRetryManager, issuePrivacyTokens } = sock;
|
|
18
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
17
19
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
18
20
|
const retryMutex = makeMutex();
|
|
19
21
|
const msgRetryCache = config.msgRetryCounterCache ||
|
|
@@ -50,19 +52,21 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
50
52
|
};
|
|
51
53
|
return sendPeerDataOperationMessage(pdoMessage);
|
|
52
54
|
};
|
|
53
|
-
const requestPlaceholderResend = async (messageKey) => {
|
|
55
|
+
const requestPlaceholderResend = async (messageKey, msgData) => {
|
|
54
56
|
if (!authState.creds.me?.id) {
|
|
55
57
|
throw new Boom('Not authenticated');
|
|
56
58
|
}
|
|
57
|
-
if (placeholderResendCache.get(messageKey?.id)) {
|
|
59
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
58
60
|
logger.debug({ messageKey }, 'already requested resend');
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
|
-
|
|
64
|
+
// Store original message data so PDO response handler can preserve
|
|
65
|
+
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
66
|
+
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
63
67
|
}
|
|
64
|
-
await delay(
|
|
65
|
-
if (!placeholderResendCache.get(messageKey?.id)) {
|
|
68
|
+
await delay(2000);
|
|
69
|
+
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
66
70
|
logger.debug({ messageKey }, 'message received while resend requested');
|
|
67
71
|
return 'RESOLVED';
|
|
68
72
|
}
|
|
@@ -75,32 +79,152 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
75
79
|
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
76
80
|
};
|
|
77
81
|
setTimeout(async () => {
|
|
78
|
-
if (placeholderResendCache.get(messageKey?.id)) {
|
|
79
|
-
logger.debug({ messageKey }, 'PDO message without response after
|
|
82
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
83
|
+
logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
|
|
80
84
|
await placeholderResendCache.del(messageKey?.id);
|
|
81
85
|
}
|
|
82
|
-
},
|
|
86
|
+
}, 8000);
|
|
83
87
|
return sendPeerDataOperationMessage(pdoMessage);
|
|
84
88
|
};
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
|
|
90
|
+
function isValidEnforcementType(value) {
|
|
91
|
+
return typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value);
|
|
92
|
+
}
|
|
93
|
+
// ── Top-level mex dispatcher (PR: feat-mex-notification-dispatch) ─────────
|
|
94
|
+
const handleMexNotification = async (node) => {
|
|
95
|
+
const updateNode = getBinaryNodeChild(node, 'update');
|
|
96
|
+
if (updateNode) {
|
|
97
|
+
const opName = updateNode.attrs?.op_name;
|
|
98
|
+
if (!opName) {
|
|
99
|
+
logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let mexResponse;
|
|
103
|
+
try {
|
|
104
|
+
mexResponse = JSON.parse(updateNode.content.toString());
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error({ err: error, opName }, 'failed to parse mex notification JSON');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (mexResponse.errors?.length) {
|
|
111
|
+
logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const data = mexResponse.data;
|
|
115
|
+
if (!data) {
|
|
116
|
+
logger.warn({ opName }, 'mex notification has null data');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
logger.debug({ opName }, 'processing mex notification');
|
|
120
|
+
switch (opName) {
|
|
121
|
+
case 'NotificationUserReachoutTimelockUpdate':
|
|
122
|
+
handleReachoutTimelockNotification(data);
|
|
123
|
+
break;
|
|
124
|
+
case 'MessageCappingInfoNotification':
|
|
125
|
+
handleMessageCappingNotification(data);
|
|
126
|
+
break;
|
|
127
|
+
case 'NotificationLinkedProfilesUpdates': {
|
|
128
|
+
// PR: fix-mex-linked-profiles
|
|
129
|
+
const linkedProfiles = data.xwa2_notify_linked_profiles;
|
|
130
|
+
if (!linkedProfiles)
|
|
131
|
+
break;
|
|
132
|
+
const lid = linkedProfiles.jid;
|
|
133
|
+
for (const profile of linkedProfiles.added_profiles ?? []) {
|
|
134
|
+
const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
|
|
135
|
+
if (lid && pn)
|
|
136
|
+
ev.emit('lid-mapping.update', { lid, pn });
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
// newsletter ops still use the legacy <mex> child structure
|
|
141
|
+
case 'NotificationNewsletterUpdate':
|
|
142
|
+
case 'NotificationNewsletterAdminPromote':
|
|
143
|
+
case 'NotificationNewsletterAdminDemote':
|
|
144
|
+
case 'NotificationNewsletterUserSettingChange':
|
|
145
|
+
case 'NotificationNewsletterJoin':
|
|
146
|
+
case 'NotificationNewsletterLeave':
|
|
147
|
+
case 'NotificationNewsletterStateChange':
|
|
148
|
+
case 'NotificationNewsletterAdminMetadataUpdate':
|
|
149
|
+
case 'NotificationNewsletterOwnerUpdate':
|
|
150
|
+
case 'NotificationNewsletterAdminInviteRevoke':
|
|
151
|
+
case 'NotificationNewsletterWamoSubStatusChange':
|
|
152
|
+
case 'NotificationNewsletterBlockUser':
|
|
153
|
+
case 'NotificationNewsletterPaidPartnership':
|
|
154
|
+
case 'NotificationNewsletterMilestone':
|
|
155
|
+
case 'NewsletterResponseStateUpdate':
|
|
156
|
+
await handleLegacyMexNewsletterNotification(node);
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
logger.debug({ opName }, 'unhandled mex notification');
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await handleLegacyMexNewsletterNotification(node);
|
|
165
|
+
};
|
|
166
|
+
const handleReachoutTimelockNotification = (data) => {
|
|
167
|
+
const payload = data.xwa2_notify_account_reachout_timelock;
|
|
168
|
+
if (!payload) {
|
|
169
|
+
logger.warn('reachout timelock notification missing payload');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!payload.is_active) {
|
|
173
|
+
logger.info('reachout timelock restriction lifted');
|
|
174
|
+
ev.emit('connection.update', {
|
|
175
|
+
reachoutTimeLock: {
|
|
176
|
+
isActive: false,
|
|
177
|
+
enforcementType: ReachoutTimelockEnforcementType.DEFAULT
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// WA Web defaults to now+60s when the server omits the expiry
|
|
183
|
+
const timeEnforcementEnds = payload.time_enforcement_ends
|
|
184
|
+
? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
|
|
185
|
+
: new Date(Date.now() + 60000);
|
|
186
|
+
const enforcementType = isValidEnforcementType(payload.enforcement_type)
|
|
187
|
+
? payload.enforcement_type
|
|
188
|
+
: ReachoutTimelockEnforcementType.DEFAULT;
|
|
189
|
+
logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
|
|
190
|
+
ev.emit('connection.update', {
|
|
191
|
+
reachoutTimeLock: {
|
|
192
|
+
isActive: true,
|
|
193
|
+
timeEnforcementEnds,
|
|
194
|
+
enforcementType
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const handleMessageCappingNotification = (data) => {
|
|
199
|
+
const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
|
|
200
|
+
if (!payload) {
|
|
201
|
+
logger.warn('message capping notification missing payload');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
logger.info({ payload }, 'received message capping update');
|
|
205
|
+
ev.emit('message-capping.update', payload);
|
|
206
|
+
};
|
|
207
|
+
// ── Legacy mex newsletter notification handler ────────────────────────────
|
|
208
|
+
const handleLegacyMexNewsletterNotification = async (node) => {
|
|
87
209
|
const mexNode = getBinaryNodeChild(node, 'mex');
|
|
88
210
|
if (!mexNode?.content) {
|
|
89
|
-
logger.warn({ node }, '
|
|
211
|
+
logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification');
|
|
90
212
|
return;
|
|
91
213
|
}
|
|
92
|
-
let
|
|
214
|
+
let parsed;
|
|
93
215
|
try {
|
|
94
|
-
|
|
216
|
+
// PR fix-mex-linked-profiles: handle binary-encoded content correctly
|
|
217
|
+
const payloadContent = mexNode.content;
|
|
218
|
+
const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
|
|
219
|
+
parsed = JSON.parse(contentBuf.toString());
|
|
95
220
|
}
|
|
96
221
|
catch (error) {
|
|
97
|
-
logger.error({ err: error, node }, '
|
|
222
|
+
logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
|
|
98
223
|
return;
|
|
99
224
|
}
|
|
100
|
-
const operation =
|
|
101
|
-
const updates = data?.updates;
|
|
225
|
+
const { operation, updates } = parsed;
|
|
102
226
|
if (!updates || !operation) {
|
|
103
|
-
logger.warn({
|
|
227
|
+
logger.warn({ parsed }, 'invalid mex newsletter notification content');
|
|
104
228
|
return;
|
|
105
229
|
}
|
|
106
230
|
logger.info({ operation, updates }, 'got mex newsletter notification');
|
|
@@ -129,7 +253,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
129
253
|
}
|
|
130
254
|
break;
|
|
131
255
|
default:
|
|
132
|
-
logger.info({ operation,
|
|
256
|
+
logger.info({ operation, parsed }, 'unhandled mex newsletter notification');
|
|
133
257
|
break;
|
|
134
258
|
}
|
|
135
259
|
};
|
|
@@ -243,26 +367,275 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
243
367
|
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
|
|
244
368
|
await sendNode(stanza);
|
|
245
369
|
};
|
|
370
|
+
// ── Call handlers ─────────────────────────────────────────────────
|
|
246
371
|
const rejectCall = async (callId, callFrom) => {
|
|
247
|
-
|
|
372
|
+
await query({
|
|
248
373
|
tag: 'call',
|
|
249
|
-
attrs: {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
374
|
+
attrs: { from: authState.creds.me.id, to: callFrom },
|
|
375
|
+
content: [
|
|
376
|
+
{ tag: 'reject', attrs: { 'call-id': callId, 'call-creator': callFrom, count: '0' }, content: undefined }
|
|
377
|
+
]
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
const offerCall = async (toJid, isVideo = false) => {
|
|
381
|
+
const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64);
|
|
382
|
+
const offerContent = [];
|
|
383
|
+
if (isVideo) {
|
|
384
|
+
offerContent.push({
|
|
385
|
+
tag: 'video',
|
|
386
|
+
attrs: {
|
|
387
|
+
enc: 'vp8',
|
|
388
|
+
dec: 'vp8',
|
|
389
|
+
orientation: '0',
|
|
390
|
+
screen_width: '1920',
|
|
391
|
+
screen_height: '1080',
|
|
392
|
+
device_orientation: '0'
|
|
393
|
+
},
|
|
394
|
+
content: undefined
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined });
|
|
398
|
+
offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined });
|
|
399
|
+
offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined });
|
|
400
|
+
offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) });
|
|
401
|
+
offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
|
|
402
|
+
const encKey = randomBytes(32);
|
|
403
|
+
const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
|
|
404
|
+
await assertSessions(devices, true);
|
|
405
|
+
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, { call: { callKey: new Uint8Array(encKey) } }, { count: '0' });
|
|
406
|
+
offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
|
|
407
|
+
if (shouldIncludeDeviceIdentity) {
|
|
408
|
+
offerContent.push({
|
|
409
|
+
tag: 'device-identity',
|
|
410
|
+
attrs: {},
|
|
411
|
+
content: encodeSignedDeviceIdentity(authState.creds.account, true)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
await query({
|
|
415
|
+
tag: 'call',
|
|
416
|
+
attrs: { id: generateMessageTag(), to: toJid },
|
|
417
|
+
content: [
|
|
418
|
+
{ tag: 'offer', attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id }, content: offerContent }
|
|
419
|
+
]
|
|
420
|
+
});
|
|
421
|
+
return { id: callId, to: toJid };
|
|
422
|
+
};
|
|
423
|
+
const initiateCall = async (jid, options = {}) => {
|
|
424
|
+
const meId = authState.creds.me?.id;
|
|
425
|
+
if (!meId)
|
|
426
|
+
throw new Boom('Not authenticated');
|
|
427
|
+
const isVideo = !!options.isVideo;
|
|
428
|
+
const isGroup = isJidGroup(jid);
|
|
429
|
+
const result = await offerCall(jid, isVideo);
|
|
430
|
+
const callId = result.id;
|
|
431
|
+
await callOfferCache.set(callId, {
|
|
432
|
+
chatId: jid,
|
|
433
|
+
from: meId,
|
|
434
|
+
id: callId,
|
|
435
|
+
date: new Date(),
|
|
436
|
+
offline: false,
|
|
437
|
+
status: 'offer',
|
|
438
|
+
isVideo,
|
|
439
|
+
isGroup,
|
|
440
|
+
groupJid: isGroup ? jid : undefined
|
|
441
|
+
});
|
|
442
|
+
return { callId, to: jid, isVideo };
|
|
443
|
+
};
|
|
444
|
+
const terminateCall = async (callId, callTo, callCreator, reason, duration) => {
|
|
445
|
+
const meId = authState.creds.me?.id;
|
|
446
|
+
if (!meId)
|
|
447
|
+
throw new Boom('Not authenticated', { statusCode: 401 });
|
|
448
|
+
const attrs = { 'call-id': callId, 'call-creator': callCreator || meId };
|
|
449
|
+
if (reason)
|
|
450
|
+
attrs.reason = reason;
|
|
451
|
+
if (typeof duration === 'number') {
|
|
452
|
+
attrs.duration = String(duration);
|
|
453
|
+
attrs.audio_duration = String(duration);
|
|
454
|
+
}
|
|
455
|
+
await query({
|
|
456
|
+
tag: 'call',
|
|
457
|
+
attrs: { to: callTo, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
458
|
+
content: [{ tag: 'terminate', attrs, content: undefined }]
|
|
459
|
+
});
|
|
460
|
+
await callOfferCache.del(callId);
|
|
461
|
+
};
|
|
462
|
+
const cancelCall = async (callId, callTo) => terminateCall(callId, callTo);
|
|
463
|
+
const acceptCall = async (callId, callFrom, isVideo) => {
|
|
464
|
+
const meId = authState.creds.me?.id;
|
|
465
|
+
if (!meId)
|
|
466
|
+
throw new Boom('Not authenticated', { statusCode: 401 });
|
|
467
|
+
const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
|
|
468
|
+
if (isVideo)
|
|
469
|
+
content.push({ tag: 'video', attrs: { dec: 'H264,AV1', device_orientation: '1' }, content: undefined });
|
|
470
|
+
content.push({ tag: 'net', attrs: { medium: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
|
|
471
|
+
await query({
|
|
472
|
+
tag: 'call',
|
|
473
|
+
attrs: { from: meId, to: callFrom, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
474
|
+
content: [{ tag: 'accept', attrs: { 'call-id': callId, 'call-creator': callFrom }, content }]
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
const preacceptCall = async (callId, callCreator, isVideo) => {
|
|
478
|
+
const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
|
|
479
|
+
if (isVideo) {
|
|
480
|
+
content.push({
|
|
481
|
+
tag: 'video',
|
|
482
|
+
attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
|
|
483
|
+
content: undefined
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
content.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined });
|
|
487
|
+
await query({
|
|
488
|
+
tag: 'call',
|
|
489
|
+
attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
490
|
+
content: [{ tag: 'preaccept', attrs: { 'call-id': callId, 'call-creator': callCreator }, content }]
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const sendRelayLatency = async (callId, callCreator, relays, transactionId) => {
|
|
494
|
+
const attrs = { 'call-id': callId, 'call-creator': callCreator };
|
|
495
|
+
if (transactionId)
|
|
496
|
+
attrs['transaction-id'] = transactionId;
|
|
497
|
+
await sendNode({
|
|
498
|
+
tag: 'call',
|
|
499
|
+
attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
500
|
+
content: [
|
|
501
|
+
{
|
|
502
|
+
tag: 'relaylatency',
|
|
503
|
+
attrs,
|
|
504
|
+
content: relays.map(r => {
|
|
505
|
+
const a = {};
|
|
506
|
+
if (r.relayName)
|
|
507
|
+
a.relay_name = r.relayName;
|
|
508
|
+
a.latency = String(r.latency);
|
|
509
|
+
if (r.relayId)
|
|
510
|
+
a.relay_id = r.relayId;
|
|
511
|
+
if (r.dlBw !== undefined)
|
|
512
|
+
a.dl_bw = String(r.dlBw);
|
|
513
|
+
if (r.ulBw !== undefined)
|
|
514
|
+
a.ul_bw = String(r.ulBw);
|
|
515
|
+
return { tag: 'te', attrs: a, content: undefined };
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
const sendTransport = async (callId, callCreator, to, candidates, round) => {
|
|
522
|
+
const attrs = {
|
|
523
|
+
'call-id': callId,
|
|
524
|
+
'call-creator': callCreator,
|
|
525
|
+
'transport-message-type': '1'
|
|
526
|
+
};
|
|
527
|
+
if (round !== undefined)
|
|
528
|
+
attrs['p2p-cand-round'] = String(round);
|
|
529
|
+
await sendNode({
|
|
530
|
+
tag: 'call',
|
|
531
|
+
attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
532
|
+
content: [
|
|
533
|
+
{
|
|
534
|
+
tag: 'transport',
|
|
535
|
+
attrs,
|
|
536
|
+
content: candidates.map(c => ({ tag: 'te', attrs: { priority: c.priority }, content: c.data }))
|
|
537
|
+
}
|
|
538
|
+
]
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
const sendCallDuration = async (callId, callCreator, peer, audioDuration, callType = '1x1') => {
|
|
542
|
+
await sendNode({
|
|
543
|
+
tag: 'call',
|
|
544
|
+
attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
|
|
253
545
|
content: [
|
|
254
546
|
{
|
|
255
|
-
tag: '
|
|
547
|
+
tag: 'duration',
|
|
256
548
|
attrs: {
|
|
257
549
|
'call-id': callId,
|
|
258
|
-
'call-creator':
|
|
259
|
-
|
|
550
|
+
'call-creator': callCreator,
|
|
551
|
+
peer,
|
|
552
|
+
audio_duration: String(audioDuration),
|
|
553
|
+
type: callType
|
|
260
554
|
},
|
|
261
555
|
content: undefined
|
|
262
556
|
}
|
|
263
557
|
]
|
|
264
|
-
};
|
|
265
|
-
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
const muteCall = async (callId, callCreator, to, muted) => {
|
|
561
|
+
await sendNode({
|
|
562
|
+
tag: 'call',
|
|
563
|
+
attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
tag: 'mute_v2',
|
|
567
|
+
attrs: { 'mute-state': muted ? '1' : '0', 'call-id': callId, 'call-creator': callCreator },
|
|
568
|
+
content: undefined
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
const sendHeartbeat = async (callId, callCreator) => {
|
|
574
|
+
await sendNode({
|
|
575
|
+
tag: 'call',
|
|
576
|
+
attrs: { to: `${callId}@call`, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
577
|
+
content: [{ tag: 'heartbeat', attrs: { 'call-id': callId, 'call-creator': callCreator }, content: undefined }]
|
|
578
|
+
});
|
|
579
|
+
};
|
|
580
|
+
const sendEncRekey = async (callId, callCreator, to, transactionId) => {
|
|
581
|
+
await sendNode({
|
|
582
|
+
tag: 'call',
|
|
583
|
+
attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
tag: 'enc_rekey',
|
|
587
|
+
attrs: { 'transaction-id': transactionId, 'call-id': callId, 'call-creator': callCreator },
|
|
588
|
+
content: [
|
|
589
|
+
{ tag: 'encopt', attrs: { keygen: '2' }, content: undefined },
|
|
590
|
+
{ tag: 'enc', attrs: { v: '2', type: 'msg' }, content: undefined }
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
const sendVideoState = async (callId, callCreator, to, enabled, orientation = '1') => {
|
|
597
|
+
await sendNode({
|
|
598
|
+
tag: 'call',
|
|
599
|
+
attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
|
|
600
|
+
content: [
|
|
601
|
+
{
|
|
602
|
+
tag: 'video',
|
|
603
|
+
attrs: {
|
|
604
|
+
'call-id': callId,
|
|
605
|
+
'call-creator': callCreator,
|
|
606
|
+
state: enabled ? '1' : '0',
|
|
607
|
+
device_orientation: orientation
|
|
608
|
+
},
|
|
609
|
+
content: undefined
|
|
610
|
+
}
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
const queryCallLink = async (token, media = 'video') => {
|
|
615
|
+
return await query({
|
|
616
|
+
tag: 'call',
|
|
617
|
+
attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
|
|
618
|
+
content: [{ tag: 'link_query', attrs: { media, token }, content: undefined }]
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
const joinCallLink = async (token, media = 'video') => {
|
|
622
|
+
const content = [
|
|
623
|
+
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
|
|
624
|
+
{ tag: 'net', attrs: { medium: '2' }, content: undefined },
|
|
625
|
+
{ tag: 'capability', attrs: { ver: '1' }, content: undefined }
|
|
626
|
+
];
|
|
627
|
+
if (media === 'video') {
|
|
628
|
+
content.splice(1, 0, {
|
|
629
|
+
tag: 'video',
|
|
630
|
+
attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
|
|
631
|
+
content: undefined
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return await query({
|
|
635
|
+
tag: 'call',
|
|
636
|
+
attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
|
|
637
|
+
content: [{ tag: 'link_join', attrs: { media, token }, content }]
|
|
638
|
+
});
|
|
266
639
|
};
|
|
267
640
|
const sendRetryRequest = async (node, forceIncludeKeys = false) => {
|
|
268
641
|
const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
|
|
@@ -300,12 +673,12 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
300
673
|
// Check if we should recreate the session
|
|
301
674
|
let shouldRecreateSession = false;
|
|
302
675
|
let recreateReason = '';
|
|
303
|
-
if (enableAutoSessionRecreation && messageRetryManager) {
|
|
676
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
304
677
|
try {
|
|
305
678
|
// Check if we have a session with this JID
|
|
306
679
|
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
|
|
307
680
|
const hasSession = await signalRepository.validateSession(fromJid);
|
|
308
|
-
const result = messageRetryManager.shouldRecreateSession(fromJid,
|
|
681
|
+
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
|
|
309
682
|
shouldRecreateSession = result.recreate;
|
|
310
683
|
recreateReason = result.reason;
|
|
311
684
|
if (shouldRecreateSession) {
|
|
@@ -407,22 +780,16 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
407
780
|
}
|
|
408
781
|
}
|
|
409
782
|
else {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
783
|
+
const result = await handleIdentityChange(node, {
|
|
784
|
+
meId: authState.creds.me?.id,
|
|
785
|
+
meLid: authState.creds.me?.lid,
|
|
786
|
+
validateSession: signalRepository.validateSession,
|
|
787
|
+
assertSessions,
|
|
788
|
+
debounceCache: identityAssertDebounce,
|
|
789
|
+
logger,
|
|
790
|
+
onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
|
|
791
|
+
});
|
|
792
|
+
if (result.action === 'no_identity_node') {
|
|
426
793
|
logger.info({ node }, 'unknown encrypt notification');
|
|
427
794
|
}
|
|
428
795
|
}
|
|
@@ -431,6 +798,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
431
798
|
// TODO: Support PN/LID (Here is only LID now)
|
|
432
799
|
const actingParticipantLid = fullNode.attrs.participant;
|
|
433
800
|
const actingParticipantPn = fullNode.attrs.participant_pn;
|
|
801
|
+
const actingParticipantUsername = fullNode.attrs.participant_username;
|
|
434
802
|
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
|
|
435
803
|
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
|
|
436
804
|
switch (child?.tag) {
|
|
@@ -450,7 +818,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
450
818
|
{
|
|
451
819
|
...metadata,
|
|
452
820
|
author: actingParticipantLid,
|
|
453
|
-
authorPn: actingParticipantPn
|
|
821
|
+
authorPn: actingParticipantPn,
|
|
822
|
+
authorUsername: actingParticipantUsername
|
|
454
823
|
}
|
|
455
824
|
]);
|
|
456
825
|
break;
|
|
@@ -556,24 +925,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
556
925
|
const nodeType = node.attrs.type;
|
|
557
926
|
const from = jidNormalizedUser(node.attrs.from);
|
|
558
927
|
switch (nodeType) {
|
|
559
|
-
case 'privacy_token':
|
|
560
|
-
const tokenList = getBinaryNodeChildren(child, 'token');
|
|
561
|
-
for (const { attrs, content } of tokenList) {
|
|
562
|
-
const jid = attrs.jid;
|
|
563
|
-
ev.emit('chats.update', [
|
|
564
|
-
{
|
|
565
|
-
id: jid,
|
|
566
|
-
tcToken: content
|
|
567
|
-
}
|
|
568
|
-
]);
|
|
569
|
-
logger.debug({ jid }, 'got privacy token update');
|
|
570
|
-
}
|
|
571
|
-
break;
|
|
572
928
|
case 'newsletter':
|
|
573
929
|
await handleNewsletterNotification(node);
|
|
574
930
|
break;
|
|
575
931
|
case 'mex':
|
|
576
|
-
await
|
|
932
|
+
await handleMexNotification(node);
|
|
577
933
|
break;
|
|
578
934
|
case 'w:gp2':
|
|
579
935
|
// TODO: HANDLE PARTICIPANT_PN
|
|
@@ -605,6 +961,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
605
961
|
case 'picture':
|
|
606
962
|
const setPicture = getBinaryNodeChild(node, 'set');
|
|
607
963
|
const delPicture = getBinaryNodeChild(node, 'delete');
|
|
964
|
+
// TODO: WAJIDHASH stuff proper support inhouse
|
|
608
965
|
ev.emit('contacts.update', [
|
|
609
966
|
{
|
|
610
967
|
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
|
@@ -657,7 +1014,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
657
1014
|
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
|
|
658
1015
|
const random = randomBytes(32);
|
|
659
1016
|
const linkCodeSalt = randomBytes(32);
|
|
660
|
-
const linkCodePairingExpanded =
|
|
1017
|
+
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
|
661
1018
|
salt: linkCodeSalt,
|
|
662
1019
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
|
663
1020
|
});
|
|
@@ -671,7 +1028,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
671
1028
|
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
|
|
672
1029
|
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
|
|
673
1030
|
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
|
|
674
|
-
authState.creds.advSecretKey = (
|
|
1031
|
+
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
|
|
675
1032
|
await query({
|
|
676
1033
|
tag: 'iq',
|
|
677
1034
|
attrs: {
|
|
@@ -718,27 +1075,91 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
718
1075
|
return result;
|
|
719
1076
|
}
|
|
720
1077
|
};
|
|
1078
|
+
/**
|
|
1079
|
+
* In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
|
|
1080
|
+
*/
|
|
1081
|
+
const tcTokenKnownJids = new Set();
|
|
1082
|
+
const tcTokenIndexLoaded = (async () => {
|
|
1083
|
+
try {
|
|
1084
|
+
const jids = await readTcTokenIndex(authState.keys);
|
|
1085
|
+
for (const jid of jids)
|
|
1086
|
+
tcTokenKnownJids.add(jid);
|
|
1087
|
+
logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
logger.warn({ err: err?.message }, 'failed to load tctoken index');
|
|
1091
|
+
}
|
|
1092
|
+
})();
|
|
1093
|
+
let tcTokenIndexTimer;
|
|
1094
|
+
async function flushTcTokenIndex() {
|
|
1095
|
+
if (tcTokenIndexTimer) {
|
|
1096
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1097
|
+
tcTokenIndexTimer = undefined;
|
|
1098
|
+
}
|
|
1099
|
+
const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
|
|
1100
|
+
return authState.keys.set({ tctoken: write });
|
|
1101
|
+
}
|
|
1102
|
+
function scheduleTcTokenIndexSave() {
|
|
1103
|
+
if (tcTokenIndexTimer) {
|
|
1104
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1105
|
+
}
|
|
1106
|
+
tcTokenIndexTimer = setTimeout(() => {
|
|
1107
|
+
tcTokenIndexTimer = undefined;
|
|
1108
|
+
flushTcTokenIndex().catch(err => {
|
|
1109
|
+
logger.warn({ err: err?.message }, 'failed to save tctoken index');
|
|
1110
|
+
});
|
|
1111
|
+
}, 5000);
|
|
1112
|
+
}
|
|
1113
|
+
function trackTcTokenJid(jid) {
|
|
1114
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
|
|
1115
|
+
tcTokenKnownJids.add(jid);
|
|
1116
|
+
scheduleTcTokenIndexSave();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
721
1119
|
const handlePrivacyTokenNotification = async (node) => {
|
|
722
1120
|
const tokensNode = getBinaryNodeChild(node, 'tokens');
|
|
723
|
-
const from = jidNormalizedUser(node.attrs.from);
|
|
724
1121
|
if (!tokensNode)
|
|
725
1122
|
return;
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1123
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
1124
|
+
const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
|
|
1125
|
+
? jidNormalizedUser(node.attrs.sender_lid)
|
|
1126
|
+
: undefined;
|
|
1127
|
+
const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
|
|
1128
|
+
logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
|
|
1129
|
+
await storeTcTokensFromIqResult({
|
|
1130
|
+
result: node,
|
|
1131
|
+
fallbackJid,
|
|
1132
|
+
keys: authState.keys,
|
|
1133
|
+
getLIDForPN,
|
|
1134
|
+
onNewJidStored: trackTcTokenJid
|
|
1135
|
+
});
|
|
1136
|
+
};
|
|
1137
|
+
/**
|
|
1138
|
+
* Fire-and-forget tctoken re-issuance after a peer's device identity changed.
|
|
1139
|
+
*/
|
|
1140
|
+
const reissueTcTokenAfterIdentityChange = (from) => {
|
|
1141
|
+
void (async () => {
|
|
1142
|
+
const normalizedJid = jidNormalizedUser(from);
|
|
1143
|
+
const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
|
|
1144
|
+
const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
|
|
1145
|
+
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
|
|
1146
|
+
if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
|
|
1147
|
+
return;
|
|
740
1148
|
}
|
|
741
|
-
|
|
1149
|
+
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
|
|
1150
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
1151
|
+
const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
|
|
1152
|
+
const result = await issuePrivacyTokens([issueJid], senderTs);
|
|
1153
|
+
await storeTcTokensFromIqResult({
|
|
1154
|
+
result,
|
|
1155
|
+
fallbackJid: tcJid,
|
|
1156
|
+
keys: authState.keys,
|
|
1157
|
+
getLIDForPN,
|
|
1158
|
+
onNewJidStored: trackTcTokenJid
|
|
1159
|
+
});
|
|
1160
|
+
})().catch(err => {
|
|
1161
|
+
logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
|
|
1162
|
+
});
|
|
742
1163
|
};
|
|
743
1164
|
async function decipherLinkPublicKey(data) {
|
|
744
1165
|
const buffer = toRequiredBuffer(data);
|
|
@@ -802,11 +1223,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
802
1223
|
// Check if we should recreate session for this retry
|
|
803
1224
|
let shouldRecreateSession = false;
|
|
804
1225
|
let recreateReason = '';
|
|
805
|
-
if (enableAutoSessionRecreation && messageRetryManager) {
|
|
1226
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
806
1227
|
try {
|
|
807
1228
|
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
808
1229
|
const hasSession = await signalRepository.validateSession(participant);
|
|
809
|
-
const result = messageRetryManager.shouldRecreateSession(participant,
|
|
1230
|
+
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
|
|
810
1231
|
shouldRecreateSession = result.recreate;
|
|
811
1232
|
recreateReason = result.reason;
|
|
812
1233
|
if (shouldRecreateSession) {
|
|
@@ -869,7 +1290,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
869
1290
|
}
|
|
870
1291
|
try {
|
|
871
1292
|
await Promise.all([
|
|
872
|
-
|
|
1293
|
+
receiptMutex.mutex(async () => {
|
|
873
1294
|
const status = getStatusFromReceiptType(attrs.type);
|
|
874
1295
|
if (typeof status !== 'undefined' &&
|
|
875
1296
|
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
|
@@ -890,7 +1311,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
890
1311
|
else {
|
|
891
1312
|
ev.emit('messages.update', ids.map(id => ({
|
|
892
1313
|
key: { ...key, id },
|
|
893
|
-
update: { status }
|
|
1314
|
+
update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
|
|
894
1315
|
})));
|
|
895
1316
|
}
|
|
896
1317
|
}
|
|
@@ -933,7 +1354,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
933
1354
|
}
|
|
934
1355
|
try {
|
|
935
1356
|
await Promise.all([
|
|
936
|
-
|
|
1357
|
+
notificationMutex.mutex(async () => {
|
|
937
1358
|
const msg = await processNotification(node);
|
|
938
1359
|
if (msg) {
|
|
939
1360
|
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
|
|
@@ -967,88 +1388,158 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
967
1388
|
}
|
|
968
1389
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
969
1390
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
970
|
-
if (encNode
|
|
1391
|
+
if (encNode?.attrs.type === 'msmsg') {
|
|
971
1392
|
logger.debug({ key: node.attrs.key }, 'ignored msmsg');
|
|
972
1393
|
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
|
|
973
1394
|
return;
|
|
974
1395
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
await signalRepository.
|
|
1396
|
+
let acked = false;
|
|
1397
|
+
try {
|
|
1398
|
+
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
|
|
1399
|
+
const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
|
|
1400
|
+
// store new mappings we didn't have before
|
|
1401
|
+
if (!!alt) {
|
|
1402
|
+
const altServer = jidDecode(alt)?.server;
|
|
1403
|
+
const primaryJid = msg.key.participant || msg.key.remoteJid;
|
|
1404
|
+
if (altServer === 'lid') {
|
|
1405
|
+
if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
|
|
1406
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
|
|
1407
|
+
await signalRepository.migrateSession(primaryJid, alt);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
|
|
1412
|
+
await signalRepository.migrateSession(alt, primaryJid);
|
|
985
1413
|
}
|
|
986
1414
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1415
|
+
// Cache for retry receipts BEFORE decrypt — so retry logic works even if decryption throws
|
|
1416
|
+
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
|
1417
|
+
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
1418
|
+
logger.debug({ jid: msg.key.remoteJid, id: msg.key.id }, 'Added message to recent cache for retry receipts');
|
|
990
1419
|
}
|
|
991
|
-
|
|
992
|
-
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
|
993
|
-
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
994
|
-
logger.debug({
|
|
995
|
-
jid: msg.key.remoteJid,
|
|
996
|
-
id: msg.key.id
|
|
997
|
-
}, 'Added message to recent cache for retry receipts');
|
|
998
|
-
}
|
|
999
|
-
try {
|
|
1000
|
-
await processingMutex.mutex(async () => {
|
|
1420
|
+
await messageMutex.mutex(async () => {
|
|
1001
1421
|
await decrypt();
|
|
1002
1422
|
// message failed to decrypt
|
|
1003
1423
|
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
1004
1424
|
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
|
1425
|
+
acked = true;
|
|
1005
1426
|
return sendMessageAck(node, NACK_REASONS.ParsingError);
|
|
1006
1427
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1428
|
+
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
1429
|
+
// Message arrived without encryption (e.g. CTWA ads messages).
|
|
1430
|
+
// Check if this is eligible for placeholder resend (matching WA Web filters).
|
|
1431
|
+
const unavailableNode = getBinaryNodeChild(node, 'unavailable');
|
|
1432
|
+
const unavailableType = unavailableNode?.attrs?.type;
|
|
1433
|
+
if (unavailableType === 'bot_unavailable_fanout' ||
|
|
1434
|
+
unavailableType === 'hosted_unavailable_fanout' ||
|
|
1435
|
+
unavailableType === 'view_once_unavailable_fanout') {
|
|
1436
|
+
logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
|
|
1437
|
+
acked = true;
|
|
1438
|
+
return sendMessageAck(node);
|
|
1439
|
+
}
|
|
1440
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1441
|
+
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
|
|
1442
|
+
logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
|
|
1443
|
+
acked = true;
|
|
1444
|
+
return sendMessageAck(node);
|
|
1445
|
+
}
|
|
1446
|
+
// Request the real content from the phone via placeholder resend PDO.
|
|
1447
|
+
// Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
|
|
1448
|
+
// and store the requestId in stubParameters[1] so users can correlate
|
|
1449
|
+
// with the incoming PDO response event.
|
|
1450
|
+
const cleanKey = {
|
|
1451
|
+
remoteJid: msg.key.remoteJid,
|
|
1452
|
+
fromMe: msg.key.fromMe,
|
|
1453
|
+
id: msg.key.id,
|
|
1454
|
+
participant: msg.key.participant
|
|
1455
|
+
};
|
|
1456
|
+
// Cache the original message metadata so the PDO response handler
|
|
1457
|
+
// can preserve key fields (LID details etc.) that the phone may omit
|
|
1458
|
+
const msgData = {
|
|
1459
|
+
key: msg.key,
|
|
1460
|
+
messageTimestamp: msg.messageTimestamp,
|
|
1461
|
+
pushName: msg.pushName,
|
|
1462
|
+
participant: msg.participant,
|
|
1463
|
+
verifiedBizName: msg.verifiedBizName
|
|
1464
|
+
};
|
|
1465
|
+
requestPlaceholderResend(cleanKey, msgData)
|
|
1466
|
+
.then(requestId => {
|
|
1467
|
+
if (requestId && requestId !== 'RESOLVED') {
|
|
1468
|
+
logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
|
|
1469
|
+
ev.emit('messages.update', [
|
|
1470
|
+
{
|
|
1471
|
+
key: msg.key,
|
|
1472
|
+
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
|
|
1473
|
+
}
|
|
1474
|
+
]);
|
|
1029
1475
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1476
|
+
})
|
|
1477
|
+
.catch(err => {
|
|
1478
|
+
logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
|
|
1479
|
+
});
|
|
1480
|
+
acked = true;
|
|
1481
|
+
await sendMessageAck(node);
|
|
1482
|
+
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
// Skip retry for expired status messages (>24h old)
|
|
1486
|
+
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
1487
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1488
|
+
if (messageAge > STATUS_EXPIRY_SECONDS) {
|
|
1489
|
+
logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
|
|
1490
|
+
acked = true;
|
|
1491
|
+
return sendMessageAck(node);
|
|
1034
1492
|
}
|
|
1035
1493
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1494
|
+
const errorMessage = msg?.messageStubParameters?.[0] || '';
|
|
1495
|
+
const isPreKeyError = errorMessage.includes('PreKey');
|
|
1496
|
+
logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
|
|
1497
|
+
// Handle both pre-key and normal retries in single mutex
|
|
1498
|
+
await retryMutex.mutex(async () => {
|
|
1039
1499
|
try {
|
|
1500
|
+
if (!ws.isOpen) {
|
|
1501
|
+
logger.debug({ node }, 'Connection closed, skipping retry');
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
// Handle pre-key errors with upload and delay
|
|
1505
|
+
if (isPreKeyError) {
|
|
1506
|
+
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
|
|
1507
|
+
try {
|
|
1508
|
+
logger.debug('Uploading pre-keys for error recovery');
|
|
1509
|
+
await uploadPreKeys(5);
|
|
1510
|
+
logger.debug('Waiting for server to process new pre-keys');
|
|
1511
|
+
await delay(1000);
|
|
1512
|
+
}
|
|
1513
|
+
catch (uploadErr) {
|
|
1514
|
+
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1040
1517
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1041
1518
|
await sendRetryRequest(node, !encNode);
|
|
1519
|
+
if (retryRequestDelayMs) {
|
|
1520
|
+
await delay(retryRequestDelayMs);
|
|
1521
|
+
}
|
|
1042
1522
|
}
|
|
1043
|
-
catch (
|
|
1044
|
-
logger.error({
|
|
1523
|
+
catch (err) {
|
|
1524
|
+
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
|
|
1525
|
+
// Still attempt retry even if pre-key upload failed
|
|
1526
|
+
try {
|
|
1527
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1528
|
+
await sendRetryRequest(node, !encNode);
|
|
1529
|
+
}
|
|
1530
|
+
catch (retryErr) {
|
|
1531
|
+
logger.error({ retryErr }, 'Failed to send retry after error handling');
|
|
1532
|
+
}
|
|
1045
1533
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1534
|
+
acked = true;
|
|
1535
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1049
1538
|
}
|
|
1050
1539
|
else {
|
|
1051
|
-
|
|
1540
|
+
if (messageRetryManager && msg.key.id) {
|
|
1541
|
+
messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
|
|
1542
|
+
}
|
|
1052
1543
|
const isNewsletter = isJidNewsletter(msg.key.remoteJid);
|
|
1053
1544
|
if (!isNewsletter) {
|
|
1054
1545
|
// no type in the receipt => message delivered
|
|
@@ -1069,15 +1560,18 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1069
1560
|
else if (!sendActiveReceipts) {
|
|
1070
1561
|
type = 'inactive';
|
|
1071
1562
|
}
|
|
1563
|
+
acked = true;
|
|
1072
1564
|
await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
|
|
1073
1565
|
// send ack for history message
|
|
1074
1566
|
const isAnyHistoryMsg = getHistoryMsg(msg.message);
|
|
1075
1567
|
if (isAnyHistoryMsg) {
|
|
1076
1568
|
const jid = jidNormalizedUser(msg.key.remoteJid);
|
|
1077
|
-
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
|
|
1569
|
+
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
|
|
1078
1570
|
}
|
|
1079
1571
|
}
|
|
1080
1572
|
else {
|
|
1573
|
+
acked = true;
|
|
1574
|
+
await sendMessageAck(node);
|
|
1081
1575
|
logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
|
|
1082
1576
|
}
|
|
1083
1577
|
}
|
|
@@ -1087,6 +1581,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1087
1581
|
}
|
|
1088
1582
|
catch (error) {
|
|
1089
1583
|
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
|
|
1584
|
+
if (!acked) {
|
|
1585
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
|
|
1586
|
+
}
|
|
1090
1587
|
}
|
|
1091
1588
|
};
|
|
1092
1589
|
const handleCall = async (node) => {
|
|
@@ -1101,6 +1598,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1101
1598
|
const call = {
|
|
1102
1599
|
chatId: attrs.from,
|
|
1103
1600
|
from,
|
|
1601
|
+
callerPn: infoChild.attrs['caller_pn'],
|
|
1104
1602
|
id: callId,
|
|
1105
1603
|
date: new Date(+attrs.t * 1000),
|
|
1106
1604
|
offline: !!attrs.offline,
|
|
@@ -1117,6 +1615,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1117
1615
|
if (existingCall) {
|
|
1118
1616
|
call.isVideo = existingCall.isVideo;
|
|
1119
1617
|
call.isGroup = existingCall.isGroup;
|
|
1618
|
+
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
1120
1619
|
}
|
|
1121
1620
|
// delete data once call has ended
|
|
1122
1621
|
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
@@ -1127,23 +1626,22 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1127
1626
|
};
|
|
1128
1627
|
const handleBadAck = async ({ attrs }) => {
|
|
1129
1628
|
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
|
|
1130
|
-
// WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
|
|
1131
|
-
// // current hypothesis is that if pash is sent in the ack
|
|
1132
|
-
// // it means -- the message hasn't reached all devices yet
|
|
1133
|
-
// // we'll retry sending the message here
|
|
1134
|
-
// if(attrs.phash) {
|
|
1135
|
-
// logger.info({ attrs }, 'received phash in ack, resending message...')
|
|
1136
|
-
// const msg = await getMessage(key)
|
|
1137
|
-
// if(msg) {
|
|
1138
|
-
// await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
|
|
1139
|
-
// } else {
|
|
1140
|
-
// logger.warn({ attrs }, 'could not send message again, as it was not found')
|
|
1141
|
-
// }
|
|
1142
|
-
// }
|
|
1143
1629
|
// error in acknowledgement,
|
|
1144
1630
|
// device could not display the message
|
|
1145
1631
|
if (attrs.error) {
|
|
1146
|
-
|
|
1632
|
+
if (attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
|
|
1633
|
+
// 463 = account restricted + no tctoken for this contact.
|
|
1634
|
+
// WA Web prevents this client-side (disables compose bar).
|
|
1635
|
+
// No retry — retrying worsens the restriction by counting
|
|
1636
|
+
// as another "reach out" to an unknown contact.
|
|
1637
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
|
|
1638
|
+
}
|
|
1639
|
+
else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
1640
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
logger.warn({ attrs }, 'received error in ack');
|
|
1644
|
+
}
|
|
1147
1645
|
ev.emit('messages.update', [
|
|
1148
1646
|
{
|
|
1149
1647
|
key,
|
|
@@ -1153,19 +1651,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1153
1651
|
}
|
|
1154
1652
|
}
|
|
1155
1653
|
]);
|
|
1156
|
-
// resend the message with device_fanout=false, use at your own risk
|
|
1157
|
-
// if (attrs.error === '475') {
|
|
1158
|
-
// const msg = await getMessage(key)
|
|
1159
|
-
// if (msg) {
|
|
1160
|
-
// await relayMessage(key.remoteJid!, msg, {
|
|
1161
|
-
// messageId: key.id!,
|
|
1162
|
-
// useUserDevicesCache: false,
|
|
1163
|
-
// additionalAttributes: {
|
|
1164
|
-
// device_fanout: 'false'
|
|
1165
|
-
// }
|
|
1166
|
-
// })
|
|
1167
|
-
// }
|
|
1168
|
-
// }
|
|
1169
1654
|
}
|
|
1170
1655
|
};
|
|
1171
1656
|
/// processes a node with the given function
|
|
@@ -1178,6 +1663,10 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1178
1663
|
return exec(node, false).catch(err => onUnexpectedError(err, identifier));
|
|
1179
1664
|
}
|
|
1180
1665
|
};
|
|
1666
|
+
/** Yields control to the event loop to prevent blocking */
|
|
1667
|
+
const yieldToEventLoop = () => {
|
|
1668
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
1669
|
+
};
|
|
1181
1670
|
const makeOfflineNodeProcessor = () => {
|
|
1182
1671
|
const nodeProcessorMap = new Map([
|
|
1183
1672
|
['message', handleMessage],
|
|
@@ -1187,6 +1676,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1187
1676
|
]);
|
|
1188
1677
|
const nodes = [];
|
|
1189
1678
|
let isProcessing = false;
|
|
1679
|
+
// Number of nodes to process before yielding to event loop
|
|
1680
|
+
const BATCH_SIZE = 10;
|
|
1190
1681
|
const enqueue = (type, node) => {
|
|
1191
1682
|
nodes.push({ type, node });
|
|
1192
1683
|
if (isProcessing) {
|
|
@@ -1194,6 +1685,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1194
1685
|
}
|
|
1195
1686
|
isProcessing = true;
|
|
1196
1687
|
const promise = async () => {
|
|
1688
|
+
let processedInBatch = 0;
|
|
1197
1689
|
while (nodes.length && ws.isOpen) {
|
|
1198
1690
|
const { type, node } = nodes.shift();
|
|
1199
1691
|
const nodeProcessor = nodeProcessorMap.get(type);
|
|
@@ -1202,6 +1694,13 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1202
1694
|
continue;
|
|
1203
1695
|
}
|
|
1204
1696
|
await nodeProcessor(node);
|
|
1697
|
+
processedInBatch++;
|
|
1698
|
+
// Yield to event loop after processing a batch
|
|
1699
|
+
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
1700
|
+
if (processedInBatch >= BATCH_SIZE) {
|
|
1701
|
+
processedInBatch = 0;
|
|
1702
|
+
await yieldToEventLoop();
|
|
1703
|
+
}
|
|
1205
1704
|
}
|
|
1206
1705
|
isProcessing = false;
|
|
1207
1706
|
};
|
|
@@ -1266,17 +1765,112 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1266
1765
|
await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1267
1766
|
}
|
|
1268
1767
|
});
|
|
1269
|
-
|
|
1768
|
+
/** timestamp of last tctoken prune run — throttles to once per 24h */
|
|
1769
|
+
let lastTcTokenPruneTs = 0;
|
|
1770
|
+
ev.on('connection.update', ({ isOnline, connection }) => {
|
|
1270
1771
|
if (typeof isOnline !== 'undefined') {
|
|
1271
1772
|
sendActiveReceipts = isOnline;
|
|
1272
1773
|
logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
|
|
1273
1774
|
}
|
|
1775
|
+
// Flush pending tctoken index save on disconnect to avoid writing after close
|
|
1776
|
+
if (connection === 'close' && tcTokenIndexTimer) {
|
|
1777
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1778
|
+
tcTokenIndexTimer = undefined;
|
|
1779
|
+
try {
|
|
1780
|
+
void Promise.resolve(flushTcTokenIndex()).catch(() => { });
|
|
1781
|
+
}
|
|
1782
|
+
catch {
|
|
1783
|
+
/* ignore sync errors */
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
// Prune expired tctokens when coming online, at most once per 24 hours
|
|
1787
|
+
if (isOnline) {
|
|
1788
|
+
const now = Date.now();
|
|
1789
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
1790
|
+
if (now - lastTcTokenPruneTs >= DAY_MS) {
|
|
1791
|
+
lastTcTokenPruneTs = now;
|
|
1792
|
+
void pruneExpiredTcTokens();
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1274
1795
|
});
|
|
1796
|
+
async function pruneExpiredTcTokens() {
|
|
1797
|
+
try {
|
|
1798
|
+
await tcTokenIndexLoaded;
|
|
1799
|
+
const persisted = await readTcTokenIndex(authState.keys);
|
|
1800
|
+
const allJids = new Set(tcTokenKnownJids);
|
|
1801
|
+
for (const jid of persisted)
|
|
1802
|
+
allJids.add(jid);
|
|
1803
|
+
if (!allJids.size)
|
|
1804
|
+
return;
|
|
1805
|
+
const jids = [...allJids];
|
|
1806
|
+
const allTokens = await authState.keys.get('tctoken', jids);
|
|
1807
|
+
const writes = {};
|
|
1808
|
+
const survivors = new Set();
|
|
1809
|
+
let mutated = 0;
|
|
1810
|
+
for (const jid of jids) {
|
|
1811
|
+
const entry = allTokens[jid];
|
|
1812
|
+
if (!entry) {
|
|
1813
|
+
mutated++;
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
const hasPeerToken = !!entry.token?.length;
|
|
1817
|
+
const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
|
|
1818
|
+
const hasSenderTs = entry.senderTimestamp !== undefined;
|
|
1819
|
+
const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
|
|
1820
|
+
const keepPeerToken = hasPeerToken && !peerTokenExpired;
|
|
1821
|
+
const keepSenderTs = hasSenderTs && !senderTsExpired;
|
|
1822
|
+
if (!keepPeerToken && !keepSenderTs) {
|
|
1823
|
+
writes[jid] = null;
|
|
1824
|
+
mutated++;
|
|
1825
|
+
}
|
|
1826
|
+
else if (peerTokenExpired && keepSenderTs) {
|
|
1827
|
+
writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
|
|
1828
|
+
survivors.add(jid);
|
|
1829
|
+
mutated++;
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
survivors.add(jid);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (mutated === 0)
|
|
1836
|
+
return;
|
|
1837
|
+
await authState.keys.set({
|
|
1838
|
+
tctoken: {
|
|
1839
|
+
...writes,
|
|
1840
|
+
[TC_TOKEN_INDEX_KEY]: {
|
|
1841
|
+
token: Buffer.from(JSON.stringify([...survivors]))
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
tcTokenKnownJids.clear();
|
|
1846
|
+
for (const jid of survivors)
|
|
1847
|
+
tcTokenKnownJids.add(jid);
|
|
1848
|
+
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
|
|
1849
|
+
}
|
|
1850
|
+
catch (err) {
|
|
1851
|
+
logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1275
1854
|
return {
|
|
1276
1855
|
...sock,
|
|
1277
1856
|
sendMessageAck,
|
|
1278
1857
|
sendRetryRequest,
|
|
1858
|
+
offerCall,
|
|
1859
|
+
initiateCall,
|
|
1860
|
+
cancelCall,
|
|
1279
1861
|
rejectCall,
|
|
1862
|
+
acceptCall,
|
|
1863
|
+
preacceptCall,
|
|
1864
|
+
terminateCall,
|
|
1865
|
+
sendRelayLatency,
|
|
1866
|
+
sendTransport,
|
|
1867
|
+
sendCallDuration,
|
|
1868
|
+
muteCall,
|
|
1869
|
+
sendHeartbeat,
|
|
1870
|
+
sendEncRekey,
|
|
1871
|
+
sendVideoState,
|
|
1872
|
+
queryCallLink,
|
|
1873
|
+
joinCallLink,
|
|
1280
1874
|
fetchMessageHistory,
|
|
1281
1875
|
requestPlaceholderResend,
|
|
1282
1876
|
messageRetryManager
|