@alannxd/baileys 6.0.6 → 6.0.9
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/LICENSE +1 -1
- package/README.md +341 -286
- package/WAProto/GenerateStatics.sh +3 -0
- package/WAProto/WAProto.proto +6902 -0
- package/WAProto/fix-imports.js +85 -0
- package/WAProto/index.d.ts +79257 -0
- package/WAProto/index.js +205861 -60565
- package/engine-requirements.js +1 -1
- package/lib/Defaults/index.js +119 -136
- package/lib/Signal/Group/ciphertext-message.js +2 -5
- package/lib/Signal/Group/group-session-builder.js +7 -41
- package/lib/Signal/Group/group_cipher.js +37 -51
- package/lib/Signal/Group/index.js +12 -57
- package/lib/Signal/Group/keyhelper.js +7 -44
- package/lib/Signal/Group/sender-chain-key.js +7 -15
- package/lib/Signal/Group/sender-key-distribution-message.js +8 -11
- package/lib/Signal/Group/sender-key-message.js +9 -12
- package/lib/Signal/Group/sender-key-name.js +2 -5
- package/lib/Signal/Group/sender-key-record.js +9 -21
- package/lib/Signal/Group/sender-key-state.js +27 -42
- package/lib/Signal/Group/sender-message-key.js +4 -7
- package/lib/Signal/libsignal.js +347 -90
- package/lib/Signal/lid-mapping.js +277 -0
- package/lib/Socket/Client/index.js +3 -19
- package/lib/Socket/Client/types.js +11 -0
- package/lib/Socket/Client/websocket.js +54 -0
- package/lib/Socket/business.js +162 -43
- package/lib/Socket/chats.js +627 -427
- package/lib/Socket/communities.js +90 -80
- package/lib/Socket/groups.js +154 -161
- package/lib/Socket/index.js +11 -10
- package/lib/Socket/luxu.js +315 -469
- package/lib/Socket/messages-recv.js +1421 -615
- package/lib/Socket/messages-send.js +1150 -799
- package/lib/Socket/mex.js +42 -0
- package/lib/Socket/newsletter.js +152 -204
- package/lib/Socket/socket.js +544 -313
- package/lib/Store/index.js +10 -10
- package/lib/Store/keyed-db.js +108 -0
- package/lib/Store/make-cache-manager-store.js +43 -41
- package/lib/Store/make-in-memory-store.js +112 -341
- package/lib/Store/make-ordered-dictionary.js +14 -20
- package/lib/Store/object-repository.js +11 -6
- package/lib/Types/Auth.js +2 -2
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -2
- package/lib/Types/Chat.js +8 -4
- package/lib/Types/Contact.js +2 -2
- package/lib/Types/Events.js +2 -2
- package/lib/Types/GroupMetadata.js +2 -2
- package/lib/Types/Label.js +3 -5
- package/lib/Types/LabelAssociation.js +3 -5
- package/lib/Types/Message.js +11 -9
- package/lib/Types/Mex.js +37 -0
- package/lib/Types/Product.js +2 -2
- package/lib/Types/Signal.js +2 -2
- package/lib/Types/Socket.js +3 -2
- package/lib/Types/State.js +56 -2
- package/lib/Types/USync.js +2 -2
- package/lib/Types/index.js +15 -31
- package/lib/Utils/auth-utils.js +239 -143
- package/lib/Utils/browser-utils.js +48 -0
- package/lib/Utils/business.js +66 -69
- package/lib/Utils/chat-utils.js +396 -253
- package/lib/Utils/companion-reg-client-utils.js +35 -0
- package/lib/Utils/crypto.js +57 -90
- package/lib/Utils/decode-wa-message.js +236 -84
- package/lib/Utils/event-buffer.js +185 -77
- package/lib/Utils/generics.js +189 -209
- package/lib/Utils/history.js +93 -55
- package/lib/Utils/identity-change-handler.js +50 -0
- package/lib/Utils/index.js +23 -33
- package/lib/Utils/link-preview.js +16 -24
- package/lib/Utils/logger.js +3 -7
- package/lib/Utils/lt-hash.js +3 -46
- package/lib/Utils/make-mutex.js +24 -34
- package/lib/Utils/message-composer.js +273 -0
- package/lib/Utils/message-retry-manager.js +265 -0
- package/lib/Utils/messages-media.js +451 -482
- package/lib/Utils/messages.js +795 -369
- package/lib/Utils/noise-handler.js +145 -99
- package/lib/Utils/offline-node-processor.js +40 -0
- package/lib/Utils/pre-key-manager.js +106 -0
- package/lib/Utils/process-message.js +459 -150
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/signal.js +120 -72
- package/lib/Utils/stanza-ack.js +38 -0
- package/lib/Utils/sync-action-utils.js +49 -0
- package/lib/Utils/tc-token-utils.js +163 -0
- package/lib/Utils/use-multi-file-auth-state.js +29 -27
- package/lib/Utils/validate-connection.js +73 -99
- package/lib/WABinary/constants.js +1281 -20
- package/lib/WABinary/decode.js +52 -42
- package/lib/WABinary/encode.js +110 -155
- package/lib/WABinary/generic-utils.js +55 -49
- package/lib/WABinary/index.js +6 -21
- package/lib/WABinary/jid-utils.js +76 -40
- package/lib/WABinary/types.js +2 -2
- package/lib/WAM/BinaryInfo.js +2 -5
- package/lib/WAM/constants.js +19071 -11568
- package/lib/WAM/encode.js +17 -22
- package/lib/WAM/index.js +4 -19
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +33 -13
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -14
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -12
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -13
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -22
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -8
- package/lib/WAUSync/Protocols/index.js +6 -20
- package/lib/WAUSync/USyncQuery.js +44 -35
- package/lib/WAUSync/USyncUser.js +10 -5
- package/lib/WAUSync/index.js +4 -19
- package/lib/index.js +13 -36
- package/package.json +85 -51
- package/WAProto/fix-import.js +0 -29
- package/lib/Defaults/baileys-version.json +0 -3
- package/lib/Defaults/index.d.ts +0 -53
- package/lib/Defaults/phonenumber-mcc.json +0 -223
- package/lib/Signal/Group/ciphertext-message.d.ts +0 -9
- package/lib/Signal/Group/group-session-builder.d.ts +0 -14
- package/lib/Signal/Group/group_cipher.d.ts +0 -17
- package/lib/Signal/Group/index.d.ts +0 -11
- package/lib/Signal/Group/keyhelper.d.ts +0 -10
- package/lib/Signal/Group/queue-job.d.ts +0 -1
- package/lib/Signal/Group/queue-job.js +0 -57
- package/lib/Signal/Group/sender-chain-key.d.ts +0 -13
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +0 -16
- package/lib/Signal/Group/sender-key-message.d.ts +0 -18
- package/lib/Signal/Group/sender-key-name.d.ts +0 -17
- package/lib/Signal/Group/sender-key-record.d.ts +0 -30
- package/lib/Signal/Group/sender-key-state.d.ts +0 -38
- package/lib/Signal/Group/sender-message-key.d.ts +0 -11
- package/lib/Signal/libsignal.d.ts +0 -3
- package/lib/Socket/Client/abstract-socket-client.d.ts +0 -17
- package/lib/Socket/Client/abstract-socket-client.js +0 -13
- package/lib/Socket/Client/index.d.ts +0 -3
- package/lib/Socket/Client/mobile-socket-client.d.ts +0 -13
- package/lib/Socket/Client/mobile-socket-client.js +0 -65
- package/lib/Socket/Client/web-socket-client.d.ts +0 -12
- package/lib/Socket/Client/web-socket-client.js +0 -62
- package/lib/Socket/business.d.ts +0 -171
- package/lib/Socket/chats.d.ts +0 -267
- package/lib/Socket/communities.d.ts +0 -180
- package/lib/Socket/groups.d.ts +0 -115
- package/lib/Socket/index.d.ts +0 -173
- package/lib/Socket/luxu.d.ts +0 -266
- package/lib/Socket/messages-recv.d.ts +0 -161
- package/lib/Socket/messages-send.d.ts +0 -183
- package/lib/Socket/newsletter.d.ts +0 -134
- package/lib/Socket/registration.d.ts +0 -267
- package/lib/Socket/registration.js +0 -166
- package/lib/Socket/socket.d.ts +0 -44
- package/lib/Socket/usync.d.ts +0 -36
- package/lib/Socket/usync.js +0 -70
- package/lib/Store/index.d.ts +0 -3
- package/lib/Store/make-cache-manager-store.d.ts +0 -13
- package/lib/Store/make-in-memory-store.d.ts +0 -118
- package/lib/Store/make-ordered-dictionary.d.ts +0 -13
- package/lib/Store/object-repository.d.ts +0 -10
- package/lib/Types/Auth.d.ts +0 -110
- package/lib/Types/Call.d.ts +0 -13
- package/lib/Types/Chat.d.ts +0 -102
- package/lib/Types/Contact.d.ts +0 -19
- package/lib/Types/Events.d.ts +0 -157
- package/lib/Types/GroupMetadata.d.ts +0 -55
- package/lib/Types/Label.d.ts +0 -35
- package/lib/Types/LabelAssociation.d.ts +0 -29
- package/lib/Types/Message.d.ts +0 -273
- package/lib/Types/Newsletter.d.ts +0 -103
- package/lib/Types/Newsletter.js +0 -38
- package/lib/Types/Product.d.ts +0 -78
- package/lib/Types/Signal.d.ts +0 -57
- package/lib/Types/Socket.d.ts +0 -111
- package/lib/Types/State.d.ts +0 -27
- package/lib/Types/USync.d.ts +0 -25
- package/lib/Types/index.d.ts +0 -57
- package/lib/Utils/auth-utils.d.ts +0 -18
- package/lib/Utils/baileys-event-stream.d.ts +0 -16
- package/lib/Utils/baileys-event-stream.js +0 -63
- package/lib/Utils/business.d.ts +0 -22
- package/lib/Utils/chat-utils.d.ts +0 -71
- package/lib/Utils/crypto.d.ts +0 -41
- package/lib/Utils/decode-wa-message.d.ts +0 -19
- package/lib/Utils/event-buffer.d.ts +0 -35
- package/lib/Utils/generics.d.ts +0 -92
- package/lib/Utils/history.d.ts +0 -15
- package/lib/Utils/index.d.ts +0 -17
- package/lib/Utils/link-preview.d.ts +0 -21
- package/lib/Utils/logger.d.ts +0 -4
- package/lib/Utils/lt-hash.d.ts +0 -12
- package/lib/Utils/make-mutex.d.ts +0 -7
- package/lib/Utils/messages-media.d.ts +0 -116
- package/lib/Utils/messages.d.ts +0 -77
- package/lib/Utils/noise-handler.d.ts +0 -21
- package/lib/Utils/process-message.d.ts +0 -41
- package/lib/Utils/signal.d.ts +0 -32
- package/lib/Utils/use-multi-file-auth-state.d.ts +0 -13
- package/lib/Utils/validate-connection.d.ts +0 -11
- package/lib/WABinary/constants.d.ts +0 -30
- package/lib/WABinary/decode.d.ts +0 -7
- package/lib/WABinary/encode.d.ts +0 -3
- package/lib/WABinary/generic-utils.d.ts +0 -17
- package/lib/WABinary/index.d.ts +0 -5
- package/lib/WABinary/jid-utils.d.ts +0 -31
- package/lib/WABinary/types.d.ts +0 -18
- package/lib/WAM/BinaryInfo.d.ts +0 -17
- package/lib/WAM/constants.d.ts +0 -38
- package/lib/WAM/encode.d.ts +0 -3
- package/lib/WAM/index.d.ts +0 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +0 -9
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +0 -22
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +0 -12
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +0 -12
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +0 -25
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +0 -8
- package/lib/WAUSync/Protocols/index.d.ts +0 -4
- package/lib/WAUSync/USyncQuery.d.ts +0 -28
- package/lib/WAUSync/USyncUser.d.ts +0 -12
- package/lib/index.d.ts +0 -12
|
@@ -1,219 +1,599 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} = config;
|
|
26
|
-
const sock = (0, messages_send_1.makeMessagesSocket)(config);
|
|
27
|
-
const {
|
|
28
|
-
ev,
|
|
29
|
-
authState,
|
|
30
|
-
ws,
|
|
31
|
-
processingMutex,
|
|
32
|
-
signalRepository,
|
|
33
|
-
query,
|
|
34
|
-
upsertMessage,
|
|
35
|
-
resyncAppState,
|
|
36
|
-
groupMetadata,
|
|
37
|
-
onUnexpectedError,
|
|
38
|
-
assertSessions,
|
|
39
|
-
sendNode,
|
|
40
|
-
relayMessage,
|
|
41
|
-
sendReceipt,
|
|
42
|
-
uploadPreKeys,
|
|
43
|
-
createParticipantNodes,
|
|
44
|
-
getUSyncDevices,
|
|
45
|
-
sendPeerDataOperationMessage
|
|
46
|
-
} = sock;
|
|
1
|
+
import NodeCache from '@cacheable/node-cache';
|
|
2
|
+
import { Boom } from '@hapi/boom';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import Long from 'long';
|
|
5
|
+
import { proto } from '../../WAProto/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 { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, 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, generateWAMessageFromContent } from '../Utils/index.js';
|
|
9
|
+
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
|
+
import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
|
|
11
|
+
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
12
|
+
import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
|
|
13
|
+
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
14
|
+
import { extractGroupMetadata } from './groups.js';
|
|
15
|
+
import { makeMessagesSocket } from './messages-send.js';
|
|
16
|
+
const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
|
|
17
|
+
function isValidEnforcementType(value) {
|
|
18
|
+
return typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value);
|
|
19
|
+
}
|
|
20
|
+
export const makeMessagesRecvSocket = (config) => {
|
|
21
|
+
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
22
|
+
const sock = makeMessagesSocket(config);
|
|
23
|
+
const { userDevicesCache, devicesMutex, ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, sendMessage, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock, placeholderResendCache } = sock;
|
|
24
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
47
25
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
48
|
-
const retryMutex =
|
|
49
|
-
const msgRetryCache = config.msgRetryCounterCache ||
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
26
|
+
const retryMutex = makeMutex();
|
|
27
|
+
const msgRetryCache = config.msgRetryCounterCache ||
|
|
28
|
+
new NodeCache({
|
|
29
|
+
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
30
|
+
useClones: false
|
|
31
|
+
});
|
|
32
|
+
const callOfferCache = config.callOfferCache ||
|
|
33
|
+
new NodeCache({
|
|
34
|
+
stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
|
|
35
|
+
useClones: false
|
|
36
|
+
});
|
|
37
|
+
// Debounce identity-change session refreshes per JID to avoid bursts
|
|
38
|
+
const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
|
|
61
39
|
let sendActiveReceipts = false;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
attrs: {
|
|
66
|
-
id: attrs.id,
|
|
67
|
-
to: attrs.from,
|
|
68
|
-
class: tag
|
|
69
|
-
}
|
|
40
|
+
const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
|
|
41
|
+
if (!authState.creds.me?.id) {
|
|
42
|
+
throw new Boom('Not authenticated');
|
|
70
43
|
}
|
|
71
|
-
|
|
72
|
-
|
|
44
|
+
const pdoMessage = {
|
|
45
|
+
historySyncOnDemandRequest: {
|
|
46
|
+
chatJid: oldestMsgKey.remoteJid,
|
|
47
|
+
oldestMsgFromMe: oldestMsgKey.fromMe,
|
|
48
|
+
oldestMsgId: oldestMsgKey.id,
|
|
49
|
+
oldestMsgTimestampMs: oldestMsgTimestamp,
|
|
50
|
+
onDemandMsgCount: count
|
|
51
|
+
},
|
|
52
|
+
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
|
53
|
+
};
|
|
54
|
+
return sendPeerDataOperationMessage(pdoMessage);
|
|
55
|
+
};
|
|
56
|
+
const requestPlaceholderResend = async (messageKey, msgData) => {
|
|
57
|
+
if (!authState.creds.me?.id) {
|
|
58
|
+
throw new Boom('Not authenticated');
|
|
73
59
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
60
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
61
|
+
logger.debug({ messageKey }, 'already requested resend');
|
|
62
|
+
return;
|
|
76
63
|
}
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
else {
|
|
65
|
+
// Store original message data so PDO response handler can preserve
|
|
66
|
+
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
67
|
+
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
79
68
|
}
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
await delay(2000);
|
|
70
|
+
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
71
|
+
logger.debug({ messageKey }, 'message received while resend requested');
|
|
72
|
+
return 'RESOLVED';
|
|
82
73
|
}
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const pdoMessage = {
|
|
75
|
+
placeholderMessageResendRequest: [
|
|
76
|
+
{
|
|
77
|
+
messageKey
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
81
|
+
};
|
|
82
|
+
setTimeout(async () => {
|
|
83
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
84
|
+
logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
|
|
85
|
+
await placeholderResendCache.del(messageKey?.id);
|
|
86
|
+
}
|
|
87
|
+
}, 8000);
|
|
88
|
+
return sendPeerDataOperationMessage(pdoMessage);
|
|
89
|
+
};
|
|
90
|
+
const handleMexNotification = async (node) => {
|
|
91
|
+
const updateNode = getBinaryNodeChild(node, 'update');
|
|
92
|
+
if (updateNode) {
|
|
93
|
+
const opName = updateNode.attrs?.op_name;
|
|
94
|
+
if (!opName) {
|
|
95
|
+
logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name, fallback to legacy');
|
|
96
|
+
await handleLegacyMexNewsletterNotification(node);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let mexResponse;
|
|
100
|
+
try {
|
|
101
|
+
mexResponse = JSON.parse(updateNode.content.toString());
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
logger.error({ err: error, opName }, 'failed to parse mex notification JSON');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (mexResponse.errors?.length) {
|
|
108
|
+
logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const data = mexResponse.data;
|
|
112
|
+
if (!data) {
|
|
113
|
+
logger.warn({ opName }, 'mex notification has null data');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
logger.debug({ opName }, 'processing mex notification');
|
|
117
|
+
switch (opName) {
|
|
118
|
+
case 'NotificationUserReachoutTimelockUpdate':
|
|
119
|
+
handleReachoutTimelockNotification(data);
|
|
120
|
+
break;
|
|
121
|
+
case 'MessageCappingInfoNotification':
|
|
122
|
+
handleMessageCappingNotification(data);
|
|
123
|
+
break;
|
|
124
|
+
// newsletter ops still use the legacy <mex> child structure
|
|
125
|
+
case 'NotificationNewsletterUpdate':
|
|
126
|
+
case 'NotificationLinkedProfilesUpdates':
|
|
127
|
+
case 'NotificationNewsletterAdminPromote':
|
|
128
|
+
case 'NotificationNewsletterAdminDemote':
|
|
129
|
+
case 'NotificationNewsletterUserSettingChange':
|
|
130
|
+
case 'NotificationNewsletterJoin':
|
|
131
|
+
case 'NotificationNewsletterLeave':
|
|
132
|
+
case 'NotificationNewsletterStateChange':
|
|
133
|
+
case 'NotificationNewsletterAdminMetadataUpdate':
|
|
134
|
+
case 'NotificationNewsletterOwnerUpdate':
|
|
135
|
+
case 'NotificationNewsletterAdminInviteRevoke':
|
|
136
|
+
case 'NotificationNewsletterWamoSubStatusChange':
|
|
137
|
+
case 'NotificationNewsletterBlockUser':
|
|
138
|
+
case 'NotificationNewsletterPaidPartnership':
|
|
139
|
+
case 'NotificationNewsletterMilestone':
|
|
140
|
+
case 'NewsletterResponseStateUpdate':
|
|
141
|
+
await handleLegacyMexNewsletterNotification(node);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
logger.debug({ opName }, 'unhandled mex notification');
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
85
148
|
}
|
|
86
|
-
|
|
87
|
-
recv: {
|
|
88
|
-
tag,
|
|
89
|
-
attrs
|
|
90
|
-
},
|
|
91
|
-
sent: stanza.attrs }, 'sent ack');
|
|
92
|
-
await sendNode(stanza);
|
|
149
|
+
await handleLegacyMexNewsletterNotification(node);
|
|
93
150
|
};
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
attrs: {
|
|
107
|
-
enc: 'opus',
|
|
108
|
-
rate: '8000'
|
|
109
|
-
}, content: undefined
|
|
110
|
-
});
|
|
111
|
-
if (isVideo) {
|
|
112
|
-
offerContent.push({
|
|
113
|
-
tag: 'video',
|
|
114
|
-
attrs: {
|
|
115
|
-
orientation: '0',
|
|
116
|
-
'screen_width': '1920',
|
|
117
|
-
'screen_height': '1080',
|
|
118
|
-
'device_orientation': '0',
|
|
119
|
-
enc: 'vp8',
|
|
120
|
-
dec: 'vp8',
|
|
151
|
+
const handleReachoutTimelockNotification = (data) => {
|
|
152
|
+
const payload = data.xwa2_notify_account_reachout_timelock;
|
|
153
|
+
if (!payload) {
|
|
154
|
+
logger.warn('reachout timelock notification missing payload');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!payload.is_active) {
|
|
158
|
+
logger.info('reachout timelock restriction lifted');
|
|
159
|
+
ev.emit('connection.update', {
|
|
160
|
+
reachoutTimeLock: {
|
|
161
|
+
isActive: false,
|
|
162
|
+
enforcementType: ReachoutTimelockEnforcementType.DEFAULT
|
|
121
163
|
}
|
|
122
164
|
});
|
|
165
|
+
return;
|
|
123
166
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
attrs: {
|
|
138
|
-
keygen: '2'
|
|
139
|
-
}, content: undefined
|
|
140
|
-
})
|
|
141
|
-
const encKey = (0, crypto_1.randomBytes)(32);
|
|
142
|
-
const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => (0, WABinary_1.jidEncode)(user, 's.whatsapp.net', device));
|
|
143
|
-
await assertSessions(devices, true);
|
|
144
|
-
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
|
|
145
|
-
call: {
|
|
146
|
-
callKey: encKey
|
|
167
|
+
// WA Web defaults to now+60s when the server omits the expiry
|
|
168
|
+
const timeEnforcementEnds = payload.time_enforcement_ends
|
|
169
|
+
? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
|
|
170
|
+
: new Date(Date.now() + 60000);
|
|
171
|
+
const enforcementType = isValidEnforcementType(payload.enforcement_type)
|
|
172
|
+
? payload.enforcement_type
|
|
173
|
+
: ReachoutTimelockEnforcementType.DEFAULT;
|
|
174
|
+
logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
|
|
175
|
+
ev.emit('connection.update', {
|
|
176
|
+
reachoutTimeLock: {
|
|
177
|
+
isActive: true,
|
|
178
|
+
timeEnforcementEnds,
|
|
179
|
+
enforcementType
|
|
147
180
|
}
|
|
148
181
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
};
|
|
183
|
+
const handleMessageCappingNotification = (data) => {
|
|
184
|
+
const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
|
|
185
|
+
if (!payload) {
|
|
186
|
+
logger.warn('message capping notification missing payload');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
logger.info({ payload }, 'received message capping update');
|
|
190
|
+
ev.emit('message-capping.update', payload);
|
|
191
|
+
};
|
|
192
|
+
const handleLegacyMexNewsletterNotification = async (node) => {
|
|
193
|
+
const mexNode = getBinaryNodeChild(node, 'mex');
|
|
194
|
+
const updateNode = mexNode?.content ? null : getBinaryNodeChild(node, 'update') || getAllBinaryNodeChildren(node)[0];
|
|
195
|
+
const payloadNode = mexNode?.content ? mexNode : updateNode;
|
|
196
|
+
if (!payloadNode?.content) {
|
|
197
|
+
logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let data;
|
|
201
|
+
try {
|
|
202
|
+
const payloadContent = payloadNode.content;
|
|
203
|
+
if (Array.isArray(payloadContent)) {
|
|
204
|
+
logger.warn({ payloadNode }, 'invalid mex newsletter notification payload format');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
|
|
208
|
+
data = JSON.parse(contentBuf.toString());
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const operation = data?.operation ?? payloadNode?.attrs?.op_name;
|
|
215
|
+
let updates = data?.updates;
|
|
216
|
+
if (!updates) {
|
|
217
|
+
const linkedProfiles = data?.data?.xwa2_notify_linked_profiles;
|
|
218
|
+
if (linkedProfiles) {
|
|
219
|
+
updates = [linkedProfiles];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!updates || !operation) {
|
|
223
|
+
logger.warn({ data }, 'invalid mex newsletter notification content');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
logger.info({ operation, updates }, 'got mex newsletter notification');
|
|
227
|
+
switch (operation) {
|
|
228
|
+
case 'NotificationNewsletterUpdate':
|
|
229
|
+
for (const update of updates) {
|
|
230
|
+
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
|
231
|
+
ev.emit('newsletter-settings.update', {
|
|
232
|
+
id: update.jid,
|
|
233
|
+
update: update.settings
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
case 'NotificationNewsletterAdminPromote':
|
|
239
|
+
for (const update of updates) {
|
|
240
|
+
if (update.jid && update.user) {
|
|
241
|
+
ev.emit('newsletter-participants.update', {
|
|
242
|
+
id: update.jid,
|
|
243
|
+
author: node.attrs.from,
|
|
244
|
+
user: update.user,
|
|
245
|
+
new_role: 'ADMIN',
|
|
246
|
+
action: 'promote'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case 'NotificationLinkedProfilesUpdates':
|
|
252
|
+
for (const update of updates) {
|
|
253
|
+
const lid = update?.jid;
|
|
254
|
+
const addedProfiles = Array.isArray(update?.added_profiles) ? update.added_profiles : [];
|
|
255
|
+
const mappings = [];
|
|
256
|
+
for (const profile of addedProfiles) {
|
|
257
|
+
const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
|
|
258
|
+
if (lid && pn) {
|
|
259
|
+
const mapping = { lid, pn };
|
|
260
|
+
ev.emit('lid-mapping.update', mapping);
|
|
261
|
+
mappings.push(mapping);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await signalRepository.lidMapping.storeLIDPNMappings(mappings);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
logger.info({ operation, data }, 'unhandled mex newsletter notification');
|
|
269
|
+
break;
|
|
156
270
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
271
|
+
};
|
|
272
|
+
// Handles newsletter notifications
|
|
273
|
+
const handleNewsletterNotification = async (node) => {
|
|
274
|
+
const from = node.attrs.from;
|
|
275
|
+
const children = getAllBinaryNodeChildren(node);
|
|
276
|
+
const author = node.attrs.participant;
|
|
277
|
+
for (const child of children) {
|
|
278
|
+
logger.debug({ from, child }, 'got newsletter notification');
|
|
279
|
+
switch (child.tag) {
|
|
280
|
+
case 'reaction': {
|
|
281
|
+
const reactionUpdate = {
|
|
282
|
+
id: from,
|
|
283
|
+
server_id: child.attrs.message_id,
|
|
284
|
+
reaction: {
|
|
285
|
+
code: getBinaryNodeChildString(child, 'reaction'),
|
|
286
|
+
count: 1
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
ev.emit('newsletter.reaction', reactionUpdate);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'view': {
|
|
293
|
+
const viewUpdate = {
|
|
294
|
+
id: from,
|
|
295
|
+
server_id: child.attrs.message_id,
|
|
296
|
+
count: parseInt(child.content?.toString() || '0', 10)
|
|
297
|
+
};
|
|
298
|
+
ev.emit('newsletter.view', viewUpdate);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'participant': {
|
|
302
|
+
const participantUpdate = {
|
|
303
|
+
id: from,
|
|
304
|
+
author,
|
|
305
|
+
user: child.attrs.jid,
|
|
306
|
+
action: child.attrs.action,
|
|
307
|
+
new_role: child.attrs.role
|
|
308
|
+
};
|
|
309
|
+
ev.emit('newsletter-participants.update', participantUpdate);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'update': {
|
|
313
|
+
const settingsNode = getBinaryNodeChild(child, 'settings');
|
|
314
|
+
if (settingsNode) {
|
|
315
|
+
const update = {};
|
|
316
|
+
const nameNode = getBinaryNodeChild(settingsNode, 'name');
|
|
317
|
+
if (nameNode?.content)
|
|
318
|
+
update.name = nameNode.content.toString();
|
|
319
|
+
const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
|
|
320
|
+
if (descriptionNode?.content)
|
|
321
|
+
update.description = descriptionNode.content.toString();
|
|
322
|
+
ev.emit('newsletter-settings.update', {
|
|
323
|
+
id: from,
|
|
324
|
+
update
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'message': {
|
|
330
|
+
const plaintextNode = getBinaryNodeChild(child, 'plaintext');
|
|
331
|
+
if (plaintextNode?.content) {
|
|
332
|
+
try {
|
|
333
|
+
const contentBuf = typeof plaintextNode.content === 'string'
|
|
334
|
+
? Buffer.from(plaintextNode.content, 'binary')
|
|
335
|
+
: Buffer.from(plaintextNode.content);
|
|
336
|
+
const messageProto = proto.Message.decode(contentBuf).toJSON();
|
|
337
|
+
const fullMessage = proto.WebMessageInfo.fromObject({
|
|
338
|
+
key: {
|
|
339
|
+
remoteJid: from,
|
|
340
|
+
id: child.attrs.message_id || child.attrs.server_id,
|
|
341
|
+
fromMe: false // TODO: is this really true though
|
|
342
|
+
},
|
|
343
|
+
message: messageProto,
|
|
344
|
+
messageTimestamp: +child.attrs.t
|
|
345
|
+
}).toJSON();
|
|
346
|
+
await upsertMessage(fullMessage, 'append');
|
|
347
|
+
logger.debug('Processed plaintext newsletter message');
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logger.error({ error }, 'Failed to decode plaintext newsletter message');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
default:
|
|
356
|
+
logger.warn({ node, child }, 'Unknown newsletter notification child');
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const sendMessageAck = async (node, errorCode) => {
|
|
362
|
+
const stanza = buildAckStanza(node, errorCode, authState.creds.me.id);
|
|
363
|
+
logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack');
|
|
364
|
+
await sendNode(stanza);
|
|
177
365
|
};
|
|
178
366
|
const rejectCall = async (callId, callFrom) => {
|
|
179
|
-
const stanza =
|
|
367
|
+
const stanza = {
|
|
180
368
|
tag: 'call',
|
|
181
369
|
attrs: {
|
|
182
370
|
from: authState.creds.me.id,
|
|
183
|
-
to: callFrom
|
|
371
|
+
to: callFrom
|
|
184
372
|
},
|
|
185
|
-
content: [
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
186
375
|
tag: 'reject',
|
|
187
376
|
attrs: {
|
|
188
377
|
'call-id': callId,
|
|
189
378
|
'call-creator': callFrom,
|
|
190
|
-
count: '0'
|
|
379
|
+
count: '0'
|
|
191
380
|
},
|
|
192
|
-
content: undefined
|
|
193
|
-
}
|
|
194
|
-
|
|
381
|
+
content: undefined
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
};
|
|
195
385
|
await query(stanza);
|
|
196
386
|
};
|
|
387
|
+
const sendText = async (jid, text, options, quoted = null) => {
|
|
388
|
+
return sendMessage(jid, {
|
|
389
|
+
text,
|
|
390
|
+
...options
|
|
391
|
+
}, { quoted })
|
|
392
|
+
}
|
|
393
|
+
const sendImage = async (jid, image, caption, options, quoted = null) => {
|
|
394
|
+
return sendMessage(jid, {
|
|
395
|
+
image,
|
|
396
|
+
caption,
|
|
397
|
+
...options
|
|
398
|
+
}, { quoted })
|
|
399
|
+
}
|
|
400
|
+
const sendVideo = async (jid, video, caption, options, quoted = null) => {
|
|
401
|
+
return sendMessage(jid, {
|
|
402
|
+
video,
|
|
403
|
+
caption,
|
|
404
|
+
...options
|
|
405
|
+
}, { quoted })
|
|
406
|
+
}
|
|
407
|
+
const sendDocument = async (jid, document, fileName, caption, options, quoted = null) => {
|
|
408
|
+
return sendMessage(jid, {
|
|
409
|
+
document,
|
|
410
|
+
fileName,
|
|
411
|
+
caption,
|
|
412
|
+
...options
|
|
413
|
+
}, { quoted })
|
|
414
|
+
}
|
|
415
|
+
const sendAudio = async (jid, audio, options, quoted = null) => {
|
|
416
|
+
return sendMessage(jid, {
|
|
417
|
+
audio,
|
|
418
|
+
...options
|
|
419
|
+
}, { quoted })
|
|
420
|
+
}
|
|
421
|
+
const sendLocation = async (jid, name, degreesLongitude, degreesLatitude, url, address, options, quoted = null) => {
|
|
422
|
+
return sendMessage(jid, {
|
|
423
|
+
location: {
|
|
424
|
+
degreesLongitude,
|
|
425
|
+
degreesLatitude,
|
|
426
|
+
name,
|
|
427
|
+
url,
|
|
428
|
+
address
|
|
429
|
+
},
|
|
430
|
+
...options
|
|
431
|
+
}, { quoted })
|
|
432
|
+
}
|
|
433
|
+
const sendPoll = async (jid, name, pollVote = [], multiSelect = false, options, quoted = null) => {
|
|
434
|
+
const selectableCount = multiSelect ? pollVote.length : 1;
|
|
435
|
+
|
|
436
|
+
return sendMessage(jid, {
|
|
437
|
+
poll: {
|
|
438
|
+
name,
|
|
439
|
+
values: pollVote,
|
|
440
|
+
selectableCount
|
|
441
|
+
},
|
|
442
|
+
...options
|
|
443
|
+
}, { quoted });
|
|
444
|
+
}
|
|
445
|
+
const sendQuiz = async (
|
|
446
|
+
jid,
|
|
447
|
+
name,
|
|
448
|
+
pollVote = [],
|
|
449
|
+
answer,
|
|
450
|
+
options,
|
|
451
|
+
quoted
|
|
452
|
+
) => {
|
|
453
|
+
const poll = {
|
|
454
|
+
name,
|
|
455
|
+
values: pollVote,
|
|
456
|
+
selectableCount: 1,
|
|
457
|
+
type: "QUIZ",
|
|
458
|
+
answer: { optionName: answer }
|
|
459
|
+
}
|
|
460
|
+
return sendMessage(jid, {
|
|
461
|
+
poll,
|
|
462
|
+
...options
|
|
463
|
+
}, { quoted })
|
|
464
|
+
}
|
|
465
|
+
const sendPtv = (jid, ptv, options, quoted = null) => {
|
|
466
|
+
return sendMessage(jid, {
|
|
467
|
+
ptv,
|
|
468
|
+
...options
|
|
469
|
+
}, { quoted })
|
|
470
|
+
}
|
|
471
|
+
const statusMention = async (jid, content) => {
|
|
472
|
+
const msg = await generateWAMessageFromContent(jid, content, {
|
|
473
|
+
userJid: authState.creds.me.id
|
|
474
|
+
})
|
|
475
|
+
await relayMessage("status@broadcast", msg.message, {
|
|
476
|
+
statusJidList: [jid, authState.creds.me.id],
|
|
477
|
+
additionalNodes: [
|
|
478
|
+
{
|
|
479
|
+
tag: "meta",
|
|
480
|
+
attrs: {},
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
tag: "mentioned_users",
|
|
484
|
+
attrs: {},
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
tag: "to",
|
|
488
|
+
attrs: { jid },
|
|
489
|
+
content: undefined
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
]
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const mentionMsg = {
|
|
499
|
+
statusMentionMessage: {
|
|
500
|
+
message: {
|
|
501
|
+
protocolMessage: {
|
|
502
|
+
key: msg.key,
|
|
503
|
+
type: 25,
|
|
504
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const x = generateWAMessageFromContent(jid, mentionMsg, {})
|
|
511
|
+
return relayMessage(jid, x.message, {
|
|
512
|
+
messageId: x.key.id,
|
|
513
|
+
additionalNodes: [
|
|
514
|
+
{
|
|
515
|
+
tag: "meta",
|
|
516
|
+
attrs: { is_status_mention: "true" }
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
})
|
|
520
|
+
};
|
|
197
521
|
const sendRetryRequest = async (node, forceIncludeKeys = false) => {
|
|
198
|
-
const { fullMessage } =
|
|
522
|
+
const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
|
|
199
523
|
const { key: msgKey } = fullMessage;
|
|
200
524
|
const msgId = msgKey.id;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
525
|
+
if (messageRetryManager) {
|
|
526
|
+
// Check if we've exceeded max retries using the new system
|
|
527
|
+
if (messageRetryManager.hasExceededMaxRetries(msgId)) {
|
|
528
|
+
logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing');
|
|
529
|
+
messageRetryManager.markRetryFailed(msgId);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// Increment retry count using new system
|
|
533
|
+
const retryCount = messageRetryManager.incrementRetryCount(msgId);
|
|
534
|
+
// Use the new retry count for the rest of the logic
|
|
535
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
536
|
+
await msgRetryCache.set(key, retryCount);
|
|
207
537
|
}
|
|
208
|
-
|
|
209
|
-
|
|
538
|
+
else {
|
|
539
|
+
// Fallback to old system
|
|
540
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
541
|
+
let retryCount = (await msgRetryCache.get(key)) || 0;
|
|
542
|
+
if (retryCount >= maxMsgRetryCount) {
|
|
543
|
+
logger.debug({ retryCount, msgId }, 'reached retry limit, clearing');
|
|
544
|
+
await msgRetryCache.del(key);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
retryCount += 1;
|
|
548
|
+
await msgRetryCache.set(key, retryCount);
|
|
549
|
+
}
|
|
550
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
551
|
+
const retryCount = (await msgRetryCache.get(key)) || 1;
|
|
210
552
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
553
|
+
const fromJid = node.attrs.from;
|
|
554
|
+
// Check if we should recreate the session
|
|
555
|
+
let shouldRecreateSession = false;
|
|
556
|
+
let recreateReason = '';
|
|
557
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
558
|
+
try {
|
|
559
|
+
// Check if we have a session with this JID
|
|
560
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
|
|
561
|
+
const hasSession = await signalRepository.validateSession(fromJid);
|
|
562
|
+
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
|
|
563
|
+
shouldRecreateSession = result.recreate;
|
|
564
|
+
recreateReason = result.reason;
|
|
565
|
+
if (shouldRecreateSession) {
|
|
566
|
+
logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry');
|
|
567
|
+
// Delete existing session to force recreation
|
|
568
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
569
|
+
forceIncludeKeys = true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
logger.warn({ error, fromJid }, 'failed to check session recreation');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (retryCount <= 2) {
|
|
577
|
+
// Use new retry manager for phone requests if available
|
|
578
|
+
if (messageRetryManager) {
|
|
579
|
+
// Schedule phone request with delay (like whatsmeow)
|
|
580
|
+
messageRetryManager.schedulePhoneRequest(msgId, async () => {
|
|
581
|
+
try {
|
|
582
|
+
const requestId = await requestPlaceholderResend(msgKey);
|
|
583
|
+
logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
logger.warn({ error, msgId }, 'failed to send scheduled phone request');
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Fallback to immediate request
|
|
592
|
+
const msgId = await requestPlaceholderResend(msgKey);
|
|
593
|
+
logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
|
|
594
|
+
}
|
|
215
595
|
}
|
|
216
|
-
const deviceIdentity =
|
|
596
|
+
const deviceIdentity = encodeSignedDeviceIdentity(account, true);
|
|
217
597
|
await authState.keys.transaction(async () => {
|
|
218
598
|
const receipt = {
|
|
219
599
|
tag: 'receipt',
|
|
@@ -229,13 +609,15 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
229
609
|
count: retryCount.toString(),
|
|
230
610
|
id: node.attrs.id,
|
|
231
611
|
t: node.attrs.t,
|
|
232
|
-
v: '1'
|
|
612
|
+
v: '1',
|
|
613
|
+
// ADD ERROR FIELD
|
|
614
|
+
error: '0'
|
|
233
615
|
}
|
|
234
616
|
},
|
|
235
617
|
{
|
|
236
618
|
tag: 'registration',
|
|
237
619
|
attrs: {},
|
|
238
|
-
content:
|
|
620
|
+
content: encodeBigEndian(authState.creds.registrationId)
|
|
239
621
|
}
|
|
240
622
|
]
|
|
241
623
|
};
|
|
@@ -245,8 +627,8 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
245
627
|
if (node.attrs.participant) {
|
|
246
628
|
receipt.attrs.participant = node.attrs.participant;
|
|
247
629
|
}
|
|
248
|
-
if (retryCount > 1 || forceIncludeKeys) {
|
|
249
|
-
const { update, preKeys } = await
|
|
630
|
+
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
|
631
|
+
const { update, preKeys } = await getNextPreKeys(authState, 1);
|
|
250
632
|
const [keyId] = Object.keys(preKeys);
|
|
251
633
|
const key = preKeys[+keyId];
|
|
252
634
|
const content = receipt.content;
|
|
@@ -254,10 +636,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
254
636
|
tag: 'keys',
|
|
255
637
|
attrs: {},
|
|
256
638
|
content: [
|
|
257
|
-
{ tag: 'type', attrs: {}, content: Buffer.from(
|
|
639
|
+
{ tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
|
|
258
640
|
{ tag: 'identity', attrs: {}, content: identityKey.public },
|
|
259
|
-
|
|
260
|
-
|
|
641
|
+
xmppPreKey(key, +keyId),
|
|
642
|
+
xmppSignedPreKey(signedPreKey),
|
|
261
643
|
{ tag: 'device-identity', attrs: {}, content: deviceIdentity }
|
|
262
644
|
]
|
|
263
645
|
});
|
|
@@ -265,63 +647,119 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
265
647
|
}
|
|
266
648
|
await sendNode(receipt);
|
|
267
649
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
|
|
650
|
+
}, authState?.creds?.me?.id || 'sendRetryRequest');
|
|
651
|
+
};
|
|
652
|
+
// Mirrors WAWeb/Handle/PreKeyLow.js: skip a re-issued notification with the same stanza id.
|
|
653
|
+
const inFlightPreKeyLow = new Set();
|
|
654
|
+
/**
|
|
655
|
+
* Fire-and-forget tctoken re-issuance after a peer's device identity changed.
|
|
656
|
+
* Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
|
|
657
|
+
* the session refresh (not after it).
|
|
658
|
+
*/
|
|
659
|
+
const reissueTcTokenAfterIdentityChange = (from) => {
|
|
660
|
+
void (async () => {
|
|
661
|
+
const normalizedJid = jidNormalizedUser(from);
|
|
662
|
+
const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
|
|
663
|
+
const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
|
|
664
|
+
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
|
|
665
|
+
if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
|
|
669
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
670
|
+
const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
|
|
671
|
+
const result = await issuePrivacyTokens([issueJid], senderTs);
|
|
672
|
+
await storeTcTokensFromIqResult({
|
|
673
|
+
result,
|
|
674
|
+
fallbackJid: tcJid,
|
|
675
|
+
keys: authState.keys,
|
|
676
|
+
getLIDForPN,
|
|
677
|
+
onNewJidStored: trackTcTokenJid
|
|
678
|
+
});
|
|
679
|
+
})().catch(err => {
|
|
680
|
+
logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
|
|
268
681
|
});
|
|
269
682
|
};
|
|
270
683
|
const handleEncryptNotification = async (node) => {
|
|
271
684
|
const from = node.attrs.from;
|
|
272
|
-
if (from ===
|
|
273
|
-
const
|
|
685
|
+
if (from === S_WHATSAPP_NET) {
|
|
686
|
+
const stanzaId = node.attrs.id;
|
|
687
|
+
if (stanzaId && inFlightPreKeyLow.has(stanzaId)) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const countChild = getBinaryNodeChild(node, 'count');
|
|
274
691
|
const count = +countChild.attrs.value;
|
|
275
|
-
const shouldUploadMorePreKeys = count <
|
|
692
|
+
const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT;
|
|
276
693
|
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
|
|
277
694
|
if (shouldUploadMorePreKeys) {
|
|
278
|
-
|
|
695
|
+
if (stanzaId)
|
|
696
|
+
inFlightPreKeyLow.add(stanzaId);
|
|
697
|
+
try {
|
|
698
|
+
await uploadPreKeys();
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
if (stanzaId)
|
|
702
|
+
inFlightPreKeyLow.delete(stanzaId);
|
|
703
|
+
}
|
|
279
704
|
}
|
|
280
705
|
}
|
|
281
706
|
else {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
707
|
+
const result = await handleIdentityChange(node, {
|
|
708
|
+
meId: authState.creds.me?.id,
|
|
709
|
+
meLid: authState.creds.me?.lid,
|
|
710
|
+
validateSession: signalRepository.validateSession,
|
|
711
|
+
assertSessions,
|
|
712
|
+
debounceCache: identityAssertDebounce,
|
|
713
|
+
logger,
|
|
714
|
+
onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
|
|
715
|
+
});
|
|
716
|
+
if (result.action === 'no_identity_node') {
|
|
289
717
|
logger.info({ node }, 'unknown encrypt notification');
|
|
290
718
|
}
|
|
291
719
|
}
|
|
292
720
|
};
|
|
293
|
-
const handleGroupNotification = (
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
721
|
+
const handleGroupNotification = (fullNode, child, msg) => {
|
|
722
|
+
// TODO: Support PN/LID (Here is only LID now)
|
|
723
|
+
const actingParticipantLid = fullNode.attrs.participant;
|
|
724
|
+
const actingParticipantPn = fullNode.attrs.participant_pn;
|
|
725
|
+
const actingParticipantUsername = fullNode.attrs.participant_username;
|
|
726
|
+
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
|
|
727
|
+
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
|
|
728
|
+
switch (child?.tag) {
|
|
297
729
|
case 'create':
|
|
298
|
-
const metadata =
|
|
299
|
-
msg.messageStubType =
|
|
730
|
+
const metadata = extractGroupMetadata(child);
|
|
731
|
+
msg.messageStubType = WAMessageStubType.GROUP_CREATE;
|
|
300
732
|
msg.messageStubParameters = [metadata.subject];
|
|
301
|
-
msg.key = { participant: metadata.owner };
|
|
302
|
-
ev.emit('chats.upsert', [
|
|
733
|
+
msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn };
|
|
734
|
+
ev.emit('chats.upsert', [
|
|
735
|
+
{
|
|
303
736
|
id: metadata.id,
|
|
304
737
|
name: metadata.subject,
|
|
305
|
-
conversationTimestamp: metadata.creation
|
|
306
|
-
}
|
|
307
|
-
|
|
738
|
+
conversationTimestamp: metadata.creation
|
|
739
|
+
}
|
|
740
|
+
]);
|
|
741
|
+
ev.emit('groups.upsert', [
|
|
742
|
+
{
|
|
308
743
|
...metadata,
|
|
309
|
-
author:
|
|
310
|
-
|
|
744
|
+
author: actingParticipantLid,
|
|
745
|
+
authorPn: actingParticipantPn,
|
|
746
|
+
authorUsername: actingParticipantUsername
|
|
747
|
+
}
|
|
748
|
+
]);
|
|
311
749
|
break;
|
|
312
750
|
case 'ephemeral':
|
|
313
751
|
case 'not_ephemeral':
|
|
314
752
|
msg.message = {
|
|
315
753
|
protocolMessage: {
|
|
316
|
-
type:
|
|
754
|
+
type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
|
|
317
755
|
ephemeralExpiration: +(child.attrs.expiration || 0)
|
|
318
756
|
}
|
|
319
757
|
};
|
|
320
758
|
break;
|
|
321
759
|
case 'modify':
|
|
322
|
-
const oldNumber =
|
|
760
|
+
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid);
|
|
323
761
|
msg.messageStubParameters = oldNumber || [];
|
|
324
|
-
msg.messageStubType =
|
|
762
|
+
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER;
|
|
325
763
|
break;
|
|
326
764
|
case 'promote':
|
|
327
765
|
case 'demote':
|
|
@@ -329,179 +767,224 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
329
767
|
case 'add':
|
|
330
768
|
case 'leave':
|
|
331
769
|
const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`;
|
|
332
|
-
msg.messageStubType =
|
|
333
|
-
const participants =
|
|
770
|
+
msg.messageStubType = WAMessageStubType[stubType];
|
|
771
|
+
const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
|
|
772
|
+
// TODO: Store LID MAPPINGS
|
|
773
|
+
return {
|
|
774
|
+
id: attrs.jid,
|
|
775
|
+
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
|
776
|
+
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
777
|
+
username: attrs.participant_username || attrs.username || undefined,
|
|
778
|
+
admin: (attrs.type || null)
|
|
779
|
+
};
|
|
780
|
+
});
|
|
334
781
|
if (participants.length === 1 &&
|
|
335
782
|
// if recv. "remove" message and sender removed themselves
|
|
336
783
|
// mark as left
|
|
337
|
-
(
|
|
784
|
+
(areJidsSameUser(participants[0].id, actingParticipantLid) ||
|
|
785
|
+
areJidsSameUser(participants[0].id, actingParticipantPn)) &&
|
|
338
786
|
child.tag === 'remove') {
|
|
339
|
-
msg.messageStubType =
|
|
787
|
+
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE;
|
|
340
788
|
}
|
|
341
|
-
msg.messageStubParameters = participants;
|
|
789
|
+
msg.messageStubParameters = participants.map(a => JSON.stringify(a));
|
|
342
790
|
break;
|
|
343
791
|
case 'subject':
|
|
344
|
-
msg.messageStubType =
|
|
792
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT;
|
|
345
793
|
msg.messageStubParameters = [child.attrs.subject];
|
|
346
794
|
break;
|
|
347
795
|
case 'description':
|
|
348
|
-
const description =
|
|
349
|
-
msg.messageStubType =
|
|
796
|
+
const description = getBinaryNodeChild(child, 'body')?.content?.toString();
|
|
797
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION;
|
|
350
798
|
msg.messageStubParameters = description ? [description] : undefined;
|
|
351
799
|
break;
|
|
352
800
|
case 'announcement':
|
|
353
801
|
case 'not_announcement':
|
|
354
|
-
msg.messageStubType =
|
|
355
|
-
msg.messageStubParameters = [
|
|
802
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE;
|
|
803
|
+
msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off'];
|
|
356
804
|
break;
|
|
357
805
|
case 'locked':
|
|
358
806
|
case 'unlocked':
|
|
359
|
-
msg.messageStubType =
|
|
360
|
-
msg.messageStubParameters = [
|
|
807
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT;
|
|
808
|
+
msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off'];
|
|
361
809
|
break;
|
|
362
810
|
case 'invite':
|
|
363
|
-
msg.messageStubType =
|
|
811
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK;
|
|
364
812
|
msg.messageStubParameters = [child.attrs.code];
|
|
365
813
|
break;
|
|
366
814
|
case 'member_add_mode':
|
|
367
815
|
const addMode = child.content;
|
|
368
816
|
if (addMode) {
|
|
369
|
-
msg.messageStubType =
|
|
817
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE;
|
|
370
818
|
msg.messageStubParameters = [addMode.toString()];
|
|
371
819
|
}
|
|
372
820
|
break;
|
|
373
821
|
case 'membership_approval_mode':
|
|
374
|
-
const approvalMode =
|
|
822
|
+
const approvalMode = getBinaryNodeChild(child, 'group_join');
|
|
375
823
|
if (approvalMode) {
|
|
376
|
-
msg.messageStubType =
|
|
824
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE;
|
|
377
825
|
msg.messageStubParameters = [approvalMode.attrs.state];
|
|
378
826
|
}
|
|
379
827
|
break;
|
|
380
828
|
case 'created_membership_requests':
|
|
381
|
-
msg.messageStubType =
|
|
382
|
-
msg.messageStubParameters = [
|
|
829
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
|
|
830
|
+
msg.messageStubParameters = [
|
|
831
|
+
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
|
|
832
|
+
'created',
|
|
833
|
+
child.attrs.request_method
|
|
834
|
+
];
|
|
383
835
|
break;
|
|
384
836
|
case 'revoked_membership_requests':
|
|
385
|
-
const isDenied =
|
|
386
|
-
|
|
387
|
-
msg.
|
|
388
|
-
|
|
837
|
+
const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid);
|
|
838
|
+
// TODO: LIDMAPPING SUPPORT
|
|
839
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
|
|
840
|
+
msg.messageStubParameters = [
|
|
841
|
+
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
|
|
842
|
+
isDenied ? 'revoked' : 'rejected'
|
|
843
|
+
];
|
|
389
844
|
break;
|
|
390
|
-
default:
|
|
391
|
-
// console.log("BAILEYS-DEBUG:", JSON.stringify({ ...child, content: Buffer.isBuffer(child.content) ? child.content.toString() : child.content, participant }, null, 2))
|
|
392
845
|
}
|
|
393
846
|
};
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (reactionsList) {
|
|
401
|
-
const reactions = (0, WABinary_1.getBinaryNodeChildren)(reactionsList, 'reaction');
|
|
402
|
-
if (reactions.length === 0) {
|
|
403
|
-
ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { removed: true } });
|
|
404
|
-
}
|
|
405
|
-
reactions.forEach(item => {
|
|
406
|
-
var _a, _b;
|
|
407
|
-
ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { code: (_a = item.attrs) === null || _a === void 0 ? void 0 : _a.code, count: +((_b = item.attrs) === null || _b === void 0 ? void 0 : _b.count) } });
|
|
408
|
-
});
|
|
847
|
+
const handleDevicesNotification = async (node) => {
|
|
848
|
+
const [child] = getAllBinaryNodeChildren(node);
|
|
849
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
850
|
+
if (!child) {
|
|
851
|
+
logger.debug({ from }, 'devices notification missing child, skipping');
|
|
852
|
+
return;
|
|
409
853
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
854
|
+
const tag = child.tag;
|
|
855
|
+
const deviceHash = child.attrs.device_hash;
|
|
856
|
+
const devices = getBinaryNodeChildren(child, 'device');
|
|
857
|
+
if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) {
|
|
858
|
+
const deviceJids = devices.map(d => d.attrs.jid);
|
|
859
|
+
logger.info({ deviceJids }, 'got my own devices');
|
|
414
860
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const operation = node === null || node === void 0 ? void 0 : node.attrs.op_name;
|
|
419
|
-
const content = JSON.parse((_a = node === null || node === void 0 ? void 0 : node.content) === null || _a === void 0 ? void 0 : _a.toString());
|
|
420
|
-
let contentPath;
|
|
421
|
-
if (operation === Types_1.MexOperations.PROMOTE || operation === Types_1.MexOperations.DEMOTE) {
|
|
422
|
-
let action;
|
|
423
|
-
if (operation === Types_1.MexOperations.PROMOTE) {
|
|
424
|
-
action = 'promote';
|
|
425
|
-
contentPath = content.data[Types_1.XWAPaths.PROMOTE];
|
|
426
|
-
}
|
|
427
|
-
if (operation === Types_1.MexOperations.DEMOTE) {
|
|
428
|
-
action = 'demote';
|
|
429
|
-
contentPath = content.data[Types_1.XWAPaths.DEMOTE];
|
|
430
|
-
}
|
|
431
|
-
ev.emit('newsletter-participants.update', { id, author: contentPath.actor.pn, user: contentPath.user.pn, new_role: contentPath.user_new_role, action });
|
|
861
|
+
if (!devices.length) {
|
|
862
|
+
logger.debug({ from, tag }, 'no devices in notification, skipping');
|
|
863
|
+
return;
|
|
432
864
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
865
|
+
const decoded = [];
|
|
866
|
+
for (const d of devices) {
|
|
867
|
+
const jid = d.attrs.jid;
|
|
868
|
+
if (!jid)
|
|
869
|
+
continue;
|
|
870
|
+
const parts = jidDecode(jid);
|
|
871
|
+
if (!parts) {
|
|
872
|
+
logger.debug({ jid }, 'failed to decode device jid, skipping');
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device });
|
|
436
876
|
}
|
|
877
|
+
if (!decoded.length)
|
|
878
|
+
return;
|
|
879
|
+
await devicesMutex.mutex(async () => {
|
|
880
|
+
const byUser = new Map();
|
|
881
|
+
for (const d of decoded) {
|
|
882
|
+
const list = byUser.get(d.user) || [];
|
|
883
|
+
list.push(d);
|
|
884
|
+
byUser.set(d.user, list);
|
|
885
|
+
}
|
|
886
|
+
for (const [user, entries] of byUser) {
|
|
887
|
+
if (tag === 'update') {
|
|
888
|
+
logger.debug({ user }, `${user}'s device list updated, dropping cached devices`);
|
|
889
|
+
await userDevicesCache?.del(user);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (tag === 'remove') {
|
|
893
|
+
await signalRepository.deleteSession(entries.map(e => e.jid));
|
|
894
|
+
}
|
|
895
|
+
const existingCache = (await userDevicesCache?.get(user)) || [];
|
|
896
|
+
if (!existingCache.length) {
|
|
897
|
+
// No baseline yet; skip applying the delta so getUSyncDevices can
|
|
898
|
+
// later fetch the full device list. Caching just the notification
|
|
899
|
+
// entries would make a partial list look authoritative.
|
|
900
|
+
logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh');
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const affected = new Set(entries.map(e => e.device));
|
|
904
|
+
let updatedDevices;
|
|
905
|
+
switch (tag) {
|
|
906
|
+
case 'add':
|
|
907
|
+
logger.info({ deviceHash, count: entries.length }, 'devices added');
|
|
908
|
+
updatedDevices = [
|
|
909
|
+
...existingCache.filter(d => !affected.has(d.device)),
|
|
910
|
+
...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))
|
|
911
|
+
];
|
|
912
|
+
break;
|
|
913
|
+
case 'remove':
|
|
914
|
+
logger.info({ deviceHash, count: entries.length }, 'devices removed');
|
|
915
|
+
updatedDevices = existingCache.filter(d => !affected.has(d.device));
|
|
916
|
+
break;
|
|
917
|
+
default:
|
|
918
|
+
logger.debug({ tag }, 'Unknown device list change tag');
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (updatedDevices.length === 0) {
|
|
922
|
+
await userDevicesCache?.del(user);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
await userDevicesCache?.set(user, updatedDevices);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
});
|
|
437
929
|
};
|
|
438
930
|
const processNotification = async (node) => {
|
|
439
|
-
var _a, _b;
|
|
440
931
|
const result = {};
|
|
441
|
-
const [child] =
|
|
932
|
+
const [child] = getAllBinaryNodeChildren(node);
|
|
442
933
|
const nodeType = node.attrs.type;
|
|
443
|
-
const from =
|
|
934
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
444
935
|
switch (nodeType) {
|
|
445
|
-
case 'privacy_token':
|
|
446
|
-
const tokenList = (0, WABinary_1.getBinaryNodeChildren)(child, 'token');
|
|
447
|
-
for (const { attrs, content } of tokenList) {
|
|
448
|
-
const jid = attrs.jid;
|
|
449
|
-
ev.emit('chats.update', [
|
|
450
|
-
{
|
|
451
|
-
id: jid,
|
|
452
|
-
tcToken: content
|
|
453
|
-
}
|
|
454
|
-
]);
|
|
455
|
-
logger.debug({ jid }, 'got privacy token update');
|
|
456
|
-
}
|
|
457
|
-
break;
|
|
458
936
|
case 'newsletter':
|
|
459
|
-
handleNewsletterNotification(node
|
|
937
|
+
await handleNewsletterNotification(node);
|
|
460
938
|
break;
|
|
461
939
|
case 'mex':
|
|
462
|
-
|
|
940
|
+
await handleMexNotification(node);
|
|
463
941
|
break;
|
|
464
942
|
case 'w:gp2':
|
|
465
|
-
|
|
943
|
+
// TODO: HANDLE PARTICIPANT_PN
|
|
944
|
+
handleGroupNotification(node, child, result);
|
|
466
945
|
break;
|
|
467
946
|
case 'mediaretry':
|
|
468
|
-
const event =
|
|
947
|
+
const event = decodeMediaRetryNode(node);
|
|
469
948
|
ev.emit('messages.media-update', [event]);
|
|
470
949
|
break;
|
|
471
950
|
case 'encrypt':
|
|
472
951
|
await handleEncryptNotification(node);
|
|
473
952
|
break;
|
|
474
953
|
case 'devices':
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
954
|
+
try {
|
|
955
|
+
await handleDevicesNotification(node);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
logger.error({ error, node }, 'failed to handle devices notification');
|
|
479
959
|
}
|
|
480
960
|
break;
|
|
481
961
|
case 'server_sync':
|
|
482
|
-
const update =
|
|
962
|
+
const update = getBinaryNodeChild(node, 'collection');
|
|
483
963
|
if (update) {
|
|
484
964
|
const name = update.attrs.name;
|
|
485
965
|
await resyncAppState([name], false);
|
|
486
966
|
}
|
|
487
967
|
break;
|
|
488
968
|
case 'picture':
|
|
489
|
-
const setPicture =
|
|
490
|
-
const delPicture =
|
|
491
|
-
|
|
492
|
-
|
|
969
|
+
const setPicture = getBinaryNodeChild(node, 'set');
|
|
970
|
+
const delPicture = getBinaryNodeChild(node, 'delete');
|
|
971
|
+
// TODO: WAJIDHASH stuff proper support inhouse
|
|
972
|
+
ev.emit('contacts.update', [
|
|
973
|
+
{
|
|
974
|
+
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
|
493
975
|
imgUrl: setPicture ? 'changed' : 'removed'
|
|
494
|
-
}
|
|
495
|
-
|
|
976
|
+
}
|
|
977
|
+
]);
|
|
978
|
+
if (isJidGroup(from)) {
|
|
496
979
|
const node = setPicture || delPicture;
|
|
497
|
-
result.messageStubType =
|
|
980
|
+
result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON;
|
|
498
981
|
if (setPicture) {
|
|
499
982
|
result.messageStubParameters = [setPicture.attrs.id];
|
|
500
983
|
}
|
|
501
|
-
result.participant = node
|
|
984
|
+
result.participant = node?.attrs.author;
|
|
502
985
|
result.key = {
|
|
503
|
-
...result.key || {},
|
|
504
|
-
participant: setPicture
|
|
986
|
+
...(result.key || {}),
|
|
987
|
+
participant: setPicture?.attrs.author
|
|
505
988
|
};
|
|
506
989
|
}
|
|
507
990
|
break;
|
|
@@ -515,44 +998,48 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
515
998
|
...authState.creds.accountSettings,
|
|
516
999
|
defaultDisappearingMode: {
|
|
517
1000
|
ephemeralExpiration: newDuration,
|
|
518
|
-
ephemeralSettingTimestamp: timestamp
|
|
519
|
-
}
|
|
1001
|
+
ephemeralSettingTimestamp: timestamp
|
|
1002
|
+
}
|
|
520
1003
|
}
|
|
521
1004
|
});
|
|
522
1005
|
}
|
|
523
1006
|
else if (child.tag === 'blocklist') {
|
|
524
|
-
const blocklists =
|
|
1007
|
+
const blocklists = getBinaryNodeChildren(child, 'item');
|
|
525
1008
|
for (const { attrs } of blocklists) {
|
|
526
1009
|
const blocklist = [attrs.jid];
|
|
527
|
-
const type =
|
|
1010
|
+
const type = attrs.action === 'block' ? 'add' : 'remove';
|
|
528
1011
|
ev.emit('blocklist.update', { blocklist, type });
|
|
529
1012
|
}
|
|
530
1013
|
}
|
|
531
1014
|
break;
|
|
532
1015
|
case 'link_code_companion_reg':
|
|
533
|
-
const linkCodeCompanionReg =
|
|
534
|
-
const ref = toRequiredBuffer(
|
|
535
|
-
const primaryIdentityPublicKey = toRequiredBuffer(
|
|
536
|
-
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
|
|
1016
|
+
const linkCodeCompanionReg = getBinaryNodeChild(node, 'link_code_companion_reg');
|
|
1017
|
+
const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'));
|
|
1018
|
+
const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'));
|
|
1019
|
+
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'));
|
|
537
1020
|
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
|
|
538
|
-
const companionSharedKey =
|
|
539
|
-
const random =
|
|
540
|
-
const linkCodeSalt =
|
|
541
|
-
const linkCodePairingExpanded =
|
|
1021
|
+
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
|
|
1022
|
+
const random = randomBytes(32);
|
|
1023
|
+
const linkCodeSalt = randomBytes(32);
|
|
1024
|
+
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
|
542
1025
|
salt: linkCodeSalt,
|
|
543
1026
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
|
544
1027
|
});
|
|
545
|
-
const encryptPayload = Buffer.concat([
|
|
546
|
-
|
|
547
|
-
|
|
1028
|
+
const encryptPayload = Buffer.concat([
|
|
1029
|
+
Buffer.from(authState.creds.signedIdentityKey.public),
|
|
1030
|
+
primaryIdentityPublicKey,
|
|
1031
|
+
random
|
|
1032
|
+
]);
|
|
1033
|
+
const encryptIv = randomBytes(12);
|
|
1034
|
+
const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0));
|
|
548
1035
|
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
|
|
549
|
-
const identitySharedKey =
|
|
1036
|
+
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
|
|
550
1037
|
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
|
|
551
|
-
authState.creds.advSecretKey = (
|
|
1038
|
+
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
|
|
552
1039
|
await query({
|
|
553
1040
|
tag: 'iq',
|
|
554
1041
|
attrs: {
|
|
555
|
-
to:
|
|
1042
|
+
to: S_WHATSAPP_NET,
|
|
556
1043
|
type: 'set',
|
|
557
1044
|
id: sock.generateMessageTag(),
|
|
558
1045
|
xmlns: 'md'
|
|
@@ -562,7 +1049,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
562
1049
|
tag: 'link_code_companion_reg',
|
|
563
1050
|
attrs: {
|
|
564
1051
|
jid: authState.creds.me.id,
|
|
565
|
-
stage: 'companion_finish'
|
|
1052
|
+
stage: 'companion_finish'
|
|
566
1053
|
},
|
|
567
1054
|
content: [
|
|
568
1055
|
{
|
|
@@ -586,53 +1073,208 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
586
1073
|
});
|
|
587
1074
|
authState.creds.registered = true;
|
|
588
1075
|
ev.emit('creds.update', authState.creds);
|
|
1076
|
+
break;
|
|
1077
|
+
case 'privacy_token':
|
|
1078
|
+
await handlePrivacyTokenNotification(node);
|
|
1079
|
+
break;
|
|
589
1080
|
}
|
|
590
1081
|
if (Object.keys(result).length) {
|
|
591
1082
|
return result;
|
|
592
1083
|
}
|
|
593
1084
|
};
|
|
1085
|
+
/**
|
|
1086
|
+
* In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
|
|
1087
|
+
* Used to coalesce writes during a session; pruning always re-reads the persisted index
|
|
1088
|
+
* to cover writes made by other layers (e.g. history sync).
|
|
1089
|
+
*/
|
|
1090
|
+
const tcTokenKnownJids = new Set();
|
|
1091
|
+
const tcTokenIndexLoaded = (async () => {
|
|
1092
|
+
try {
|
|
1093
|
+
const jids = await readTcTokenIndex(authState.keys);
|
|
1094
|
+
for (const jid of jids)
|
|
1095
|
+
tcTokenKnownJids.add(jid);
|
|
1096
|
+
logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
logger.warn({ err: err?.message }, 'failed to load tctoken index');
|
|
1100
|
+
}
|
|
1101
|
+
})();
|
|
1102
|
+
let tcTokenIndexTimer;
|
|
1103
|
+
async function flushTcTokenIndex() {
|
|
1104
|
+
if (tcTokenIndexTimer) {
|
|
1105
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1106
|
+
tcTokenIndexTimer = undefined;
|
|
1107
|
+
}
|
|
1108
|
+
// Merge with whatever is already persisted so we don't clobber writes from other
|
|
1109
|
+
// paths (history sync, concurrent sessions on the same store).
|
|
1110
|
+
const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
|
|
1111
|
+
return authState.keys.set({ tctoken: write });
|
|
1112
|
+
}
|
|
1113
|
+
function scheduleTcTokenIndexSave() {
|
|
1114
|
+
if (tcTokenIndexTimer) {
|
|
1115
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1116
|
+
}
|
|
1117
|
+
tcTokenIndexTimer = setTimeout(() => {
|
|
1118
|
+
tcTokenIndexTimer = undefined;
|
|
1119
|
+
flushTcTokenIndex().catch(err => {
|
|
1120
|
+
logger.warn({ err: err?.message }, 'failed to save tctoken index');
|
|
1121
|
+
});
|
|
1122
|
+
}, 5000);
|
|
1123
|
+
}
|
|
1124
|
+
function trackTcTokenJid(jid) {
|
|
1125
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
|
|
1126
|
+
tcTokenKnownJids.add(jid);
|
|
1127
|
+
scheduleTcTokenIndexSave();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const handlePrivacyTokenNotification = async (node) => {
|
|
1131
|
+
const tokensNode = getBinaryNodeChild(node, 'tokens');
|
|
1132
|
+
if (!tokensNode)
|
|
1133
|
+
return;
|
|
1134
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
1135
|
+
// WA Web uses: senderLid ?? toLid(from) for the storage key
|
|
1136
|
+
// The sender_lid attribute provides the LID directly when available
|
|
1137
|
+
const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
|
|
1138
|
+
? jidNormalizedUser(node.attrs.sender_lid)
|
|
1139
|
+
: undefined;
|
|
1140
|
+
const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
|
|
1141
|
+
logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
|
|
1142
|
+
await storeTcTokensFromIqResult({
|
|
1143
|
+
result: node,
|
|
1144
|
+
fallbackJid,
|
|
1145
|
+
keys: authState.keys,
|
|
1146
|
+
getLIDForPN,
|
|
1147
|
+
onNewJidStored: trackTcTokenJid
|
|
1148
|
+
});
|
|
1149
|
+
};
|
|
594
1150
|
async function decipherLinkPublicKey(data) {
|
|
595
1151
|
const buffer = toRequiredBuffer(data);
|
|
596
1152
|
const salt = buffer.slice(0, 32);
|
|
597
|
-
const secretKey = await
|
|
1153
|
+
const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt);
|
|
598
1154
|
const iv = buffer.slice(32, 48);
|
|
599
1155
|
const payload = buffer.slice(48, 80);
|
|
600
|
-
return
|
|
1156
|
+
return aesDecryptCTR(payload, secretKey, iv);
|
|
601
1157
|
}
|
|
602
1158
|
function toRequiredBuffer(data) {
|
|
603
1159
|
if (data === undefined) {
|
|
604
|
-
throw new
|
|
1160
|
+
throw new Boom('Invalid buffer', { statusCode: 400 });
|
|
605
1161
|
}
|
|
606
1162
|
return data instanceof Buffer ? data : Buffer.from(data);
|
|
607
1163
|
}
|
|
608
|
-
const willSendMessageAgain = (id, participant) => {
|
|
1164
|
+
const willSendMessageAgain = async (id, participant) => {
|
|
609
1165
|
const key = `${id}:${participant}`;
|
|
610
|
-
const retryCount = msgRetryCache.get(key) || 0;
|
|
1166
|
+
const retryCount = (await msgRetryCache.get(key)) || 0;
|
|
611
1167
|
return retryCount < maxMsgRetryCount;
|
|
612
1168
|
};
|
|
613
|
-
const updateSendMessageAgainCount = (id, participant) => {
|
|
1169
|
+
const updateSendMessageAgainCount = async (id, participant) => {
|
|
614
1170
|
const key = `${id}:${participant}`;
|
|
615
|
-
const newValue = (msgRetryCache.get(key) || 0) + 1;
|
|
616
|
-
msgRetryCache.set(key, newValue);
|
|
1171
|
+
const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
|
|
1172
|
+
await msgRetryCache.set(key, newValue);
|
|
617
1173
|
};
|
|
618
|
-
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
619
|
-
var _a;
|
|
620
|
-
// todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
|
|
621
|
-
const msgs = await Promise.all(ids.map(id => getMessage({ ...key, id })));
|
|
1174
|
+
const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
|
|
622
1175
|
const remoteJid = key.remoteJid;
|
|
623
1176
|
const participant = key.participant || remoteJid;
|
|
1177
|
+
const retryCount = +retryNode.attrs.count || 1;
|
|
1178
|
+
const msgId = ids[0];
|
|
1179
|
+
// Try to get messages from cache first, then fallback to getMessage
|
|
1180
|
+
const msgs = [];
|
|
1181
|
+
for (const id of ids) {
|
|
1182
|
+
let msg;
|
|
1183
|
+
// Try to get from retry cache first if enabled
|
|
1184
|
+
if (messageRetryManager) {
|
|
1185
|
+
const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id);
|
|
1186
|
+
if (cachedMsg) {
|
|
1187
|
+
msg = cachedMsg.message;
|
|
1188
|
+
logger.debug({ jid: remoteJid, id }, 'found message in retry cache');
|
|
1189
|
+
// Mark retry as successful since we found the message
|
|
1190
|
+
messageRetryManager.markRetrySuccess(id);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// Fallback to getMessage if not found in cache
|
|
1194
|
+
if (!msg) {
|
|
1195
|
+
msg = await getMessage({ ...key, id });
|
|
1196
|
+
if (msg) {
|
|
1197
|
+
logger.debug({ jid: remoteJid, id }, 'found message via getMessage');
|
|
1198
|
+
// Also mark as successful if found via getMessage
|
|
1199
|
+
if (messageRetryManager) {
|
|
1200
|
+
messageRetryManager.markRetrySuccess(id);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
msgs.push(msg);
|
|
1205
|
+
}
|
|
624
1206
|
// if it's the primary jid sending the request
|
|
625
1207
|
// just re-send the message to everyone
|
|
626
1208
|
// prevents the first message decryption failure
|
|
627
|
-
const sendToAll = !
|
|
628
|
-
|
|
629
|
-
|
|
1209
|
+
const sendToAll = !jidDecode(participant)?.device;
|
|
1210
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
1211
|
+
let injectedFromBundle = false;
|
|
1212
|
+
const bundle = extractE2ESessionFromRetryReceipt(receiptNode);
|
|
1213
|
+
if (bundle) {
|
|
1214
|
+
try {
|
|
1215
|
+
await signalRepository.injectE2ESession({ jid: participant, session: bundle });
|
|
1216
|
+
injectedFromBundle = true;
|
|
1217
|
+
logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle');
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
logger.warn({ error, participant }, 'failed to inject session from retry receipt');
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (!injectedFromBundle) {
|
|
1224
|
+
const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4);
|
|
1225
|
+
if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
|
|
1226
|
+
const info = await signalRepository.getSessionInfo(participant);
|
|
1227
|
+
if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
|
|
1228
|
+
logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session');
|
|
1229
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const BASE_KEY_CHECK_RETRY = 2;
|
|
1234
|
+
if (msgId && messageRetryManager) {
|
|
1235
|
+
const info = await signalRepository.getSessionInfo(participant);
|
|
1236
|
+
if (info) {
|
|
1237
|
+
if (retryCount === BASE_KEY_CHECK_RETRY) {
|
|
1238
|
+
messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey);
|
|
1239
|
+
}
|
|
1240
|
+
else if (retryCount > BASE_KEY_CHECK_RETRY) {
|
|
1241
|
+
if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
|
|
1242
|
+
logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session');
|
|
1243
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
1244
|
+
}
|
|
1245
|
+
messageRetryManager.deleteBaseKey(sessionId, msgId);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
let shouldRecreateSession = false;
|
|
1250
|
+
let recreateReason = '';
|
|
1251
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
|
|
1252
|
+
try {
|
|
1253
|
+
const hasSession = await signalRepository.validateSession(participant);
|
|
1254
|
+
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
|
|
1255
|
+
shouldRecreateSession = result.recreate;
|
|
1256
|
+
recreateReason = result.reason;
|
|
1257
|
+
if (shouldRecreateSession) {
|
|
1258
|
+
logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry');
|
|
1259
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (!injectedFromBundle) {
|
|
1267
|
+
await assertSessions([participant], true);
|
|
1268
|
+
}
|
|
1269
|
+
if (isJidGroup(remoteJid)) {
|
|
630
1270
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
|
|
631
1271
|
}
|
|
632
|
-
logger.debug({ participant, sendToAll }, '
|
|
1272
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, 'prepared session for retry resend');
|
|
633
1273
|
for (const [i, msg] of msgs.entries()) {
|
|
634
|
-
if (
|
|
635
|
-
|
|
1274
|
+
if (!ids[i])
|
|
1275
|
+
continue;
|
|
1276
|
+
if (msg && (await willSendMessageAgain(ids[i], participant))) {
|
|
1277
|
+
await updateSendMessageAgainCount(ids[i], participant);
|
|
636
1278
|
const msgRelayOpts = { messageId: ids[i] };
|
|
637
1279
|
if (sendToAll) {
|
|
638
1280
|
msgRelayOpts.useUserDevicesCache = false;
|
|
@@ -651,11 +1293,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
651
1293
|
}
|
|
652
1294
|
};
|
|
653
1295
|
const handleReceipt = async (node) => {
|
|
654
|
-
var _a, _b;
|
|
655
1296
|
const { attrs, content } = node;
|
|
656
1297
|
const isLid = attrs.from.includes('lid');
|
|
657
|
-
const isNodeFromMe =
|
|
658
|
-
const remoteJid = !isNodeFromMe ||
|
|
1298
|
+
const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
|
|
1299
|
+
const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
|
|
659
1300
|
const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe);
|
|
660
1301
|
const key = {
|
|
661
1302
|
remoteJid,
|
|
@@ -663,33 +1304,26 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
663
1304
|
fromMe,
|
|
664
1305
|
participant: attrs.participant
|
|
665
1306
|
};
|
|
666
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
|
|
667
|
-
logger.debug({ remoteJid }, 'ignoring receipt from jid');
|
|
668
|
-
await sendMessageAck(node);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
1307
|
const ids = [attrs.id];
|
|
672
1308
|
if (Array.isArray(content)) {
|
|
673
|
-
const items =
|
|
1309
|
+
const items = getBinaryNodeChildren(content[0], 'item');
|
|
674
1310
|
ids.push(...items.map(i => i.attrs.id));
|
|
675
1311
|
}
|
|
676
1312
|
try {
|
|
677
1313
|
await Promise.all([
|
|
678
|
-
|
|
679
|
-
const status =
|
|
1314
|
+
receiptMutex.mutex(async () => {
|
|
1315
|
+
const status = getStatusFromReceiptType(attrs.type);
|
|
680
1316
|
if (typeof status !== 'undefined' &&
|
|
681
|
-
(
|
|
682
1317
|
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
|
683
1318
|
// or another device of ours has read some messages
|
|
684
|
-
status >=
|
|
685
|
-
|
|
686
|
-
if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
|
|
1319
|
+
(status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
|
|
1320
|
+
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
|
687
1321
|
if (attrs.participant) {
|
|
688
|
-
const updateKey = status ===
|
|
1322
|
+
const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp';
|
|
689
1323
|
ev.emit('message-receipt.update', ids.map(id => ({
|
|
690
1324
|
key: { ...key, id },
|
|
691
1325
|
receipt: {
|
|
692
|
-
userJid:
|
|
1326
|
+
userJid: jidNormalizedUser(attrs.participant),
|
|
693
1327
|
[updateKey]: +attrs.t
|
|
694
1328
|
}
|
|
695
1329
|
})));
|
|
@@ -698,22 +1332,23 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
698
1332
|
else {
|
|
699
1333
|
ev.emit('messages.update', ids.map(id => ({
|
|
700
1334
|
key: { ...key, id },
|
|
701
|
-
update: { status }
|
|
1335
|
+
update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
|
|
702
1336
|
})));
|
|
703
1337
|
}
|
|
704
1338
|
}
|
|
705
1339
|
if (attrs.type === 'retry') {
|
|
706
1340
|
// correctly set who is asking for the retry
|
|
707
1341
|
key.participant = key.participant || attrs.from;
|
|
708
|
-
const retryNode =
|
|
709
|
-
if (willSendMessageAgain(ids[0], key.participant)) {
|
|
1342
|
+
const retryNode = getBinaryNodeChild(node, 'retry');
|
|
1343
|
+
if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
|
|
710
1344
|
if (key.fromMe) {
|
|
711
1345
|
try {
|
|
1346
|
+
await updateSendMessageAgainCount(ids[0], key.participant);
|
|
712
1347
|
logger.debug({ attrs, key }, 'recv retry request');
|
|
713
|
-
await sendMessagesAgain(key, ids, retryNode);
|
|
1348
|
+
await sendMessagesAgain(key, ids, retryNode, node);
|
|
714
1349
|
}
|
|
715
1350
|
catch (error) {
|
|
716
|
-
logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
|
|
1351
|
+
logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
|
|
717
1352
|
}
|
|
718
1353
|
}
|
|
719
1354
|
else {
|
|
@@ -728,269 +1363,338 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
728
1363
|
]);
|
|
729
1364
|
}
|
|
730
1365
|
finally {
|
|
731
|
-
await sendMessageAck(node);
|
|
1366
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack receipt'));
|
|
732
1367
|
}
|
|
733
1368
|
};
|
|
734
1369
|
const handleNotification = async (node) => {
|
|
735
1370
|
const remoteJid = node.attrs.from;
|
|
736
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
|
|
737
|
-
logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
|
|
738
|
-
await sendMessageAck(node);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
1371
|
try {
|
|
742
1372
|
await Promise.all([
|
|
743
|
-
|
|
744
|
-
var _a;
|
|
1373
|
+
notificationMutex.mutex(async () => {
|
|
745
1374
|
const msg = await processNotification(node);
|
|
746
1375
|
if (msg) {
|
|
747
|
-
const fromMe =
|
|
1376
|
+
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
|
|
1377
|
+
const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node);
|
|
748
1378
|
msg.key = {
|
|
749
1379
|
remoteJid,
|
|
750
1380
|
fromMe,
|
|
751
1381
|
participant: node.attrs.participant,
|
|
1382
|
+
participantAlt,
|
|
1383
|
+
participantUsername: node.attrs.participant_username,
|
|
1384
|
+
addressingMode,
|
|
752
1385
|
id: node.attrs.id,
|
|
753
1386
|
...(msg.key || {})
|
|
754
1387
|
};
|
|
755
|
-
|
|
1388
|
+
msg.participant ?? (msg.participant = node.attrs.participant);
|
|
756
1389
|
msg.messageTimestamp = +node.attrs.t;
|
|
757
|
-
const fullMsg =
|
|
1390
|
+
const fullMsg = proto.WebMessageInfo.fromObject(msg);
|
|
758
1391
|
await upsertMessage(fullMsg, 'append');
|
|
759
1392
|
}
|
|
760
1393
|
})
|
|
761
1394
|
]);
|
|
762
1395
|
}
|
|
763
1396
|
finally {
|
|
764
|
-
await sendMessageAck(node);
|
|
1397
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack notification'));
|
|
765
1398
|
}
|
|
766
1399
|
};
|
|
767
1400
|
const handleMessage = async (node) => {
|
|
768
|
-
|
|
769
|
-
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') {
|
|
770
|
-
logger.debug({ key: node.attrs.key }, 'ignored message');
|
|
771
|
-
await sendMessageAck(node);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc');
|
|
1401
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
775
1402
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
776
|
-
if (encNode
|
|
1403
|
+
if (encNode?.attrs.type === 'msmsg') {
|
|
777
1404
|
logger.debug({ key: node.attrs.key }, 'ignored msmsg');
|
|
778
|
-
await sendMessageAck(node);
|
|
1405
|
+
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
|
|
779
1406
|
return;
|
|
780
1407
|
}
|
|
781
|
-
let
|
|
782
|
-
if ((0, WABinary_1.getBinaryNodeChild)(node, 'unavailable') && !encNode) {
|
|
783
|
-
await sendMessageAck(node);
|
|
784
|
-
const { key } = (0, Utils_1.decodeMessageNode)(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage;
|
|
785
|
-
response = await requestPlaceholderResend(key);
|
|
786
|
-
if (response === 'RESOLVED') {
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
logger.debug('received unavailable message, acked and requested resend from phone');
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
if (placeholderResendCache.get(node.attrs.id)) {
|
|
793
|
-
placeholderResendCache.del(node.attrs.id);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const { fullMessage: msg, category, author, decrypt } = (0, Utils_1.decryptMessageNode)(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
|
|
797
|
-
if (response && ((_a = msg === null || msg === void 0 ? void 0 : msg.messageStubParameters) === null || _a === void 0 ? void 0 : _a[0]) === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
798
|
-
msg.messageStubParameters = [Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT, response];
|
|
799
|
-
}
|
|
800
|
-
if (((_c = (_b = msg.message) === null || _b === void 0 ? void 0 : _b.protocolMessage) === null || _c === void 0 ? void 0 : _c.type) === WAProto_1.proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER && node.attrs.sender_pn) {
|
|
801
|
-
ev.emit('chats.phoneNumberShare', { lid: node.attrs.from, jid: node.attrs.sender_pn });
|
|
802
|
-
}
|
|
1408
|
+
let acked = false;
|
|
803
1409
|
try {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1410
|
+
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
|
|
1411
|
+
const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
|
|
1412
|
+
// store new mappings we didn't have before
|
|
1413
|
+
if (!!alt) {
|
|
1414
|
+
const altServer = jidDecode(alt)?.server;
|
|
1415
|
+
const primaryJid = msg.key.participant || msg.key.remoteJid;
|
|
1416
|
+
if (altServer === 'lid') {
|
|
1417
|
+
if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
|
|
1418
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
|
|
1419
|
+
await signalRepository.migrateSession(primaryJid, alt);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
|
|
1424
|
+
await signalRepository.migrateSession(alt, primaryJid);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
await messageMutex.mutex(async () => {
|
|
1428
|
+
await decrypt();
|
|
1429
|
+
if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
|
|
1430
|
+
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
1431
|
+
}
|
|
1432
|
+
// message failed to decrypt
|
|
1433
|
+
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
1434
|
+
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
|
1435
|
+
acked = true;
|
|
1436
|
+
return sendMessageAck(node, NACK_REASONS.ParsingError);
|
|
1437
|
+
}
|
|
1438
|
+
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
1439
|
+
// Message arrived without encryption (e.g. CTWA ads messages).
|
|
1440
|
+
// Check if this is eligible for placeholder resend (matching WA Web filters).
|
|
1441
|
+
const unavailableNode = getBinaryNodeChild(node, 'unavailable');
|
|
1442
|
+
const unavailableType = unavailableNode?.attrs?.type;
|
|
1443
|
+
if (unavailableType === 'bot_unavailable_fanout' ||
|
|
1444
|
+
unavailableType === 'hosted_unavailable_fanout' ||
|
|
1445
|
+
unavailableType === 'view_once_unavailable_fanout') {
|
|
1446
|
+
logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
|
|
1447
|
+
acked = true;
|
|
1448
|
+
return sendMessageAck(node);
|
|
812
1449
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1450
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1451
|
+
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
|
|
1452
|
+
logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
|
|
1453
|
+
acked = true;
|
|
1454
|
+
return sendMessageAck(node);
|
|
1455
|
+
}
|
|
1456
|
+
// Request the real content from the phone via placeholder resend PDO.
|
|
1457
|
+
// Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
|
|
1458
|
+
// and store the requestId in stubParameters[1] so users can correlate
|
|
1459
|
+
// with the incoming PDO response event.
|
|
1460
|
+
const cleanKey = {
|
|
1461
|
+
remoteJid: msg.key.remoteJid,
|
|
1462
|
+
fromMe: msg.key.fromMe,
|
|
1463
|
+
id: msg.key.id,
|
|
1464
|
+
participant: msg.key.participant
|
|
1465
|
+
};
|
|
1466
|
+
// Cache the original message metadata so the PDO response handler
|
|
1467
|
+
// can preserve key fields (LID details etc.) that the phone may omit
|
|
1468
|
+
const msgData = {
|
|
1469
|
+
key: msg.key,
|
|
1470
|
+
messageTimestamp: msg.messageTimestamp,
|
|
1471
|
+
pushName: msg.pushName,
|
|
1472
|
+
participant: msg.participant,
|
|
1473
|
+
verifiedBizName: msg.verifiedBizName
|
|
1474
|
+
};
|
|
1475
|
+
requestPlaceholderResend(cleanKey, msgData)
|
|
1476
|
+
.then(requestId => {
|
|
1477
|
+
if (requestId && requestId !== 'RESOLVED') {
|
|
1478
|
+
logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
|
|
1479
|
+
ev.emit('messages.update', [
|
|
1480
|
+
{
|
|
1481
|
+
key: msg.key,
|
|
1482
|
+
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
|
|
1483
|
+
}
|
|
1484
|
+
]);
|
|
1485
|
+
}
|
|
1486
|
+
})
|
|
1487
|
+
.catch(err => {
|
|
1488
|
+
logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
|
|
1489
|
+
});
|
|
1490
|
+
acked = true;
|
|
1491
|
+
await sendMessageAck(node);
|
|
1492
|
+
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
// Skip retry for expired status messages (>24h old)
|
|
1496
|
+
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
1497
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1498
|
+
if (messageAge > STATUS_EXPIRY_SECONDS) {
|
|
1499
|
+
logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
|
|
1500
|
+
acked = true;
|
|
1501
|
+
return sendMessageAck(node);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
logger.debug('[handleMessage] Attempting retry request for failed decryption');
|
|
1505
|
+
// WAWeb only retry-receipts here; server emits PreKeyLow if prekeys run low.
|
|
1506
|
+
await retryMutex.mutex(async () => {
|
|
1507
|
+
try {
|
|
1508
|
+
if (!ws.isOpen) {
|
|
1509
|
+
logger.debug({ node }, 'Connection closed, skipping retry');
|
|
816
1510
|
return;
|
|
817
1511
|
}
|
|
818
|
-
const encNode =
|
|
1512
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
819
1513
|
await sendRetryRequest(node, !encNode);
|
|
820
1514
|
if (retryRequestDelayMs) {
|
|
821
|
-
await
|
|
1515
|
+
await delay(retryRequestDelayMs);
|
|
822
1516
|
}
|
|
823
1517
|
}
|
|
824
|
-
|
|
825
|
-
logger.
|
|
1518
|
+
catch (err) {
|
|
1519
|
+
logger.error({ err }, 'Failed to send retry');
|
|
826
1520
|
}
|
|
1521
|
+
acked = true;
|
|
1522
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
827
1523
|
});
|
|
828
1524
|
}
|
|
829
|
-
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
if (messageRetryManager && msg.key.id) {
|
|
1528
|
+
messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
|
|
1529
|
+
}
|
|
1530
|
+
const isNewsletter = isJidNewsletter(msg.key.remoteJid);
|
|
1531
|
+
if (!isNewsletter) {
|
|
830
1532
|
// no type in the receipt => message delivered
|
|
831
1533
|
let type = undefined;
|
|
832
|
-
if ((_b = msg.key.participant) === null || _b === void 0 ? void 0 : _b.endsWith('@lid')) {
|
|
833
|
-
msg.key.participant = node.attrs.participant_pn || authState.creds.me.id;
|
|
834
|
-
}
|
|
835
|
-
if ((0, WABinary_1.isJidGroup)(msg.key.remoteJid) && ((_f = (_e = (_d = (_c = msg.message) === null || _c === void 0 ? void 0 : _c.extendedTextMessage) === null || _d === void 0 ? void 0 : _d.contextInfo) === null || _e === void 0 ? void 0 : _e.participant) === null || _f === void 0 ? void 0 : _f.endsWith('@lid'))) {
|
|
836
|
-
if (msg.message.extendedTextMessage.contextInfo) {
|
|
837
|
-
const metadata = await groupMetadata(msg.key.remoteJid);
|
|
838
|
-
const sender = msg.message.extendedTextMessage.contextInfo.participant;
|
|
839
|
-
const found = metadata.participants.find(p => p.id === sender);
|
|
840
|
-
msg.message.extendedTextMessage.contextInfo.participant = (found === null || found === void 0 ? void 0 : found.jid) || sender;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
if (!(0, WABinary_1.isJidGroup)(msg.key.remoteJid) && (0, WABinary_1.isLidUser)(msg.key.remoteJid)) {
|
|
844
|
-
msg.key.remoteJid = node.attrs.sender_pn || node.attrs.peer_recipient_pn;
|
|
845
|
-
}
|
|
846
1534
|
let participant = msg.key.participant;
|
|
847
|
-
if (category === 'peer') {
|
|
1535
|
+
if (category === 'peer') {
|
|
1536
|
+
// special peer message
|
|
848
1537
|
type = 'peer_msg';
|
|
849
1538
|
}
|
|
850
|
-
else if (msg.key.fromMe) {
|
|
1539
|
+
else if (msg.key.fromMe) {
|
|
1540
|
+
// message was sent by us from a different device
|
|
851
1541
|
type = 'sender';
|
|
852
1542
|
// need to specially handle this case
|
|
853
|
-
if ((
|
|
854
|
-
participant = author;
|
|
1543
|
+
if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
|
|
1544
|
+
participant = author; // TODO: investigate sending receipts to LIDs and not PNs
|
|
855
1545
|
}
|
|
856
1546
|
}
|
|
857
1547
|
else if (!sendActiveReceipts) {
|
|
858
1548
|
type = 'inactive';
|
|
859
1549
|
}
|
|
1550
|
+
acked = true;
|
|
860
1551
|
await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
|
|
861
1552
|
// send ack for history message
|
|
862
|
-
const isAnyHistoryMsg =
|
|
1553
|
+
const isAnyHistoryMsg = getHistoryMsg(msg.message);
|
|
863
1554
|
if (isAnyHistoryMsg) {
|
|
864
|
-
const jid =
|
|
865
|
-
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
|
|
1555
|
+
const jid = jidNormalizedUser(msg.key.remoteJid);
|
|
1556
|
+
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
|
|
866
1557
|
}
|
|
867
1558
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1559
|
+
else {
|
|
1560
|
+
acked = true;
|
|
1561
|
+
await sendMessageAck(node);
|
|
1562
|
+
logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
|
|
1566
|
+
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
|
|
1567
|
+
});
|
|
873
1568
|
}
|
|
874
1569
|
catch (error) {
|
|
875
|
-
logger.error({ error, node }, 'error in handling message');
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
var _a;
|
|
880
|
-
if (!((_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
881
|
-
throw new boom_1.Boom('Not authenticated');
|
|
1570
|
+
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
|
|
1571
|
+
if (!acked) {
|
|
1572
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
|
|
1573
|
+
}
|
|
882
1574
|
}
|
|
883
|
-
const pdoMessage = {
|
|
884
|
-
historySyncOnDemandRequest: {
|
|
885
|
-
chatJid: oldestMsgKey.remoteJid,
|
|
886
|
-
oldestMsgFromMe: oldestMsgKey.fromMe,
|
|
887
|
-
oldestMsgId: oldestMsgKey.id,
|
|
888
|
-
oldestMsgTimestampMs: oldestMsgTimestamp,
|
|
889
|
-
onDemandMsgCount: count
|
|
890
|
-
},
|
|
891
|
-
peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
|
892
|
-
};
|
|
893
|
-
return sendPeerDataOperationMessage(pdoMessage);
|
|
894
1575
|
};
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1576
|
+
const handleCall = async (node) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const { attrs } = node;
|
|
1579
|
+
const [infoChild] = getAllBinaryNodeChildren(node);
|
|
1580
|
+
if (!infoChild) {
|
|
1581
|
+
throw new Boom('Missing call info in call node');
|
|
1582
|
+
}
|
|
1583
|
+
const status = getCallStatusFromNode(infoChild);
|
|
1584
|
+
const callId = infoChild.attrs['call-id'];
|
|
1585
|
+
const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
|
|
1586
|
+
const call = {
|
|
1587
|
+
chatId: attrs.from,
|
|
1588
|
+
from,
|
|
1589
|
+
callerPn: infoChild.attrs['caller_pn'],
|
|
1590
|
+
id: callId,
|
|
1591
|
+
date: new Date(+attrs.t * 1000),
|
|
1592
|
+
offline: !!attrs.offline,
|
|
1593
|
+
status
|
|
1594
|
+
};
|
|
1595
|
+
if (status === 'relaylatency') {
|
|
1596
|
+
const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms'];
|
|
1597
|
+
const latencyMs = latencyValue ? Number(latencyValue) : undefined;
|
|
1598
|
+
if (Number.isFinite(latencyMs)) {
|
|
1599
|
+
call.latencyMs = latencyMs;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (status === 'offer') {
|
|
1603
|
+
call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
|
|
1604
|
+
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
|
|
1605
|
+
call.groupJid = infoChild.attrs['group-jid'];
|
|
1606
|
+
await callOfferCache.set(call.id, call);
|
|
1607
|
+
}
|
|
1608
|
+
const existingCall = await callOfferCache.get(call.id);
|
|
1609
|
+
// use existing call info to populate this event
|
|
1610
|
+
if (existingCall) {
|
|
1611
|
+
call.isVideo = existingCall.isVideo;
|
|
1612
|
+
call.isGroup = existingCall.isGroup;
|
|
1613
|
+
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
1614
|
+
}
|
|
1615
|
+
// delete data once call has ended
|
|
1616
|
+
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
1617
|
+
await callOfferCache.del(call.id);
|
|
1618
|
+
}
|
|
1619
|
+
ev.emit('call', [call]);
|
|
903
1620
|
}
|
|
904
|
-
|
|
905
|
-
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling call');
|
|
906
1623
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
logger.debug({ messageKey }, 'message received while resend requested');
|
|
910
|
-
return 'RESOLVED';
|
|
1624
|
+
finally {
|
|
1625
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'));
|
|
911
1626
|
}
|
|
912
|
-
const pdoMessage = {
|
|
913
|
-
placeholderMessageResendRequest: [{
|
|
914
|
-
messageKey
|
|
915
|
-
}],
|
|
916
|
-
peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
917
|
-
};
|
|
918
|
-
setTimeout(() => {
|
|
919
|
-
if (placeholderResendCache.get(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id)) {
|
|
920
|
-
logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
|
|
921
|
-
placeholderResendCache.del(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id);
|
|
922
|
-
}
|
|
923
|
-
}, 15000);
|
|
924
|
-
return sendPeerDataOperationMessage(pdoMessage);
|
|
925
|
-
};
|
|
926
|
-
const handleCall = async (node) => {
|
|
927
|
-
const { attrs } = node;
|
|
928
|
-
const [infoChild] = (0, WABinary_1.getAllBinaryNodeChildren)(node);
|
|
929
|
-
const callId = infoChild.attrs['call-id'];
|
|
930
|
-
const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
|
|
931
|
-
const status = (0, Utils_1.getCallStatusFromNode)(infoChild);
|
|
932
|
-
const call = {
|
|
933
|
-
chatId: attrs.from,
|
|
934
|
-
from,
|
|
935
|
-
id: callId,
|
|
936
|
-
date: new Date(+attrs.t * 1000),
|
|
937
|
-
offline: !!attrs.offline,
|
|
938
|
-
status,
|
|
939
|
-
};
|
|
940
|
-
if (status === 'offer') {
|
|
941
|
-
call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video');
|
|
942
|
-
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
|
|
943
|
-
call.groupJid = infoChild.attrs['group-jid'];
|
|
944
|
-
callOfferCache.set(call.id, call);
|
|
945
|
-
}
|
|
946
|
-
const existingCall = callOfferCache.get(call.id);
|
|
947
|
-
// use existing call info to populate this event
|
|
948
|
-
if (existingCall) {
|
|
949
|
-
call.isVideo = existingCall.isVideo;
|
|
950
|
-
call.isGroup = existingCall.isGroup;
|
|
951
|
-
}
|
|
952
|
-
// delete data once call has ended
|
|
953
|
-
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
954
|
-
callOfferCache.del(call.id);
|
|
955
|
-
}
|
|
956
|
-
ev.emit('call', [call]);
|
|
957
|
-
await sendMessageAck(node);
|
|
958
1627
|
};
|
|
959
1628
|
const handleBadAck = async ({ attrs }) => {
|
|
960
|
-
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1629
|
+
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
|
|
1630
|
+
// WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
|
|
1631
|
+
// // current hypothesis is that if pash is sent in the ack
|
|
1632
|
+
// // it means -- the message hasn't reached all devices yet
|
|
1633
|
+
// // we'll retry sending the message here
|
|
1634
|
+
// if(attrs.phash) {
|
|
1635
|
+
// logger.info({ attrs }, 'received phash in ack, resending message...')
|
|
1636
|
+
// const msg = await getMessage(key)
|
|
1637
|
+
// if(msg) {
|
|
1638
|
+
// await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
|
|
1639
|
+
// } else {
|
|
1640
|
+
// logger.warn({ attrs }, 'could not send message again, as it was not found')
|
|
1641
|
+
// }
|
|
1642
|
+
// }
|
|
1643
|
+
// error in acknowledgement,
|
|
1644
|
+
// device could not display the message
|
|
1645
|
+
if (attrs.error) {
|
|
1646
|
+
const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked);
|
|
1647
|
+
if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
|
|
1648
|
+
// 463 = 1:1 message missing privacy token (tctoken). Usually means the
|
|
1649
|
+
// account is restricted: WhatsApp blocks starting new chats but preserves
|
|
1650
|
+
// existing ones, since established chats already carry a tctoken.
|
|
1651
|
+
// WA Web prevents this client-side (disables the compose bar).
|
|
1652
|
+
// No retry — retrying counts as another "reach out" and worsens the restriction.
|
|
1653
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
|
|
1654
|
+
const ackFrom = attrs.from;
|
|
1655
|
+
if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
|
|
1656
|
+
inFlight463Recoveries.add(ackFrom);
|
|
1657
|
+
void (async () => {
|
|
1658
|
+
try {
|
|
1659
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
1660
|
+
const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN);
|
|
1661
|
+
const issueJid = await resolveIssuanceJid(ackFrom, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
|
|
1662
|
+
const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds());
|
|
1663
|
+
await storeTcTokensFromIqResult({
|
|
1664
|
+
result,
|
|
1665
|
+
fallbackJid: tcStorageJid,
|
|
1666
|
+
keys: authState.keys,
|
|
1667
|
+
getLIDForPN,
|
|
1668
|
+
onNewJidStored: trackTcTokenJid
|
|
1669
|
+
});
|
|
1670
|
+
logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance');
|
|
1671
|
+
}
|
|
1672
|
+
catch (err) {
|
|
1673
|
+
logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance');
|
|
1674
|
+
}
|
|
1675
|
+
finally {
|
|
1676
|
+
inFlight463Recoveries.delete(ackFrom);
|
|
1677
|
+
}
|
|
1678
|
+
})();
|
|
1679
|
+
}
|
|
971
1680
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1681
|
+
else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
1682
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
|
|
1683
|
+
}
|
|
1684
|
+
else if (isReachoutTimelocked) {
|
|
1685
|
+
// user is temporarily restricted, fetch current restriction details
|
|
1686
|
+
await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'));
|
|
1687
|
+
logger.warn({ attrs }, 'received error in ack');
|
|
977
1688
|
}
|
|
978
1689
|
else {
|
|
979
|
-
logger.warn({ attrs }, '
|
|
1690
|
+
logger.warn({ attrs }, 'received error in ack');
|
|
980
1691
|
}
|
|
981
|
-
}
|
|
982
|
-
// error in acknowledgement,
|
|
983
|
-
// device could not display the message
|
|
984
|
-
if (attrs.error) {
|
|
985
|
-
logger.warn({ attrs }, 'received error in ack');
|
|
986
1692
|
ev.emit('messages.update', [
|
|
987
1693
|
{
|
|
988
1694
|
key,
|
|
989
1695
|
update: {
|
|
990
|
-
status:
|
|
991
|
-
messageStubParameters: [
|
|
992
|
-
attrs.error
|
|
993
|
-
]
|
|
1696
|
+
status: WAMessageStatus.ERROR,
|
|
1697
|
+
messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error]
|
|
994
1698
|
}
|
|
995
1699
|
}
|
|
996
1700
|
]);
|
|
@@ -1003,69 +1707,61 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1003
1707
|
await execTask();
|
|
1004
1708
|
ev.flush();
|
|
1005
1709
|
function execTask() {
|
|
1006
|
-
return exec(node, false)
|
|
1007
|
-
.catch(err => onUnexpectedError(err, identifier));
|
|
1710
|
+
return exec(node, false).catch(err => onUnexpectedError(err, identifier));
|
|
1008
1711
|
}
|
|
1009
1712
|
};
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
isProcessing = false;
|
|
1036
|
-
};
|
|
1037
|
-
promise().catch(error => onUnexpectedError(error, 'processing offline nodes'));
|
|
1038
|
-
};
|
|
1039
|
-
return { enqueue };
|
|
1040
|
-
};
|
|
1041
|
-
const offlineNodeProcessor = makeOfflineNodeProcessor();
|
|
1042
|
-
const processNode = (type, node, identifier, exec) => {
|
|
1713
|
+
const offlineNodeProcessor = makeOfflineNodeProcessor(new Map([
|
|
1714
|
+
['message', handleMessage],
|
|
1715
|
+
['call', handleCall],
|
|
1716
|
+
['receipt', handleReceipt],
|
|
1717
|
+
['notification', handleNotification]
|
|
1718
|
+
]), {
|
|
1719
|
+
isWsOpen: () => ws.isOpen,
|
|
1720
|
+
onUnexpectedError,
|
|
1721
|
+
yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
|
|
1722
|
+
});
|
|
1723
|
+
const processNode = async (type, node, identifier, exec) => {
|
|
1724
|
+
// Fast path: ack and drop ignored JIDs before entering the buffer/queue
|
|
1725
|
+
const from = node.attrs.from;
|
|
1726
|
+
let ignoreJid = from;
|
|
1727
|
+
if (type === 'receipt' && from) {
|
|
1728
|
+
const attrs = node.attrs;
|
|
1729
|
+
const isLid = attrs.from.includes('lid');
|
|
1730
|
+
const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
|
|
1731
|
+
ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
|
|
1732
|
+
}
|
|
1733
|
+
if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
|
|
1734
|
+
await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1043
1737
|
const isOffline = !!node.attrs.offline;
|
|
1044
1738
|
if (isOffline) {
|
|
1045
1739
|
offlineNodeProcessor.enqueue(type, node);
|
|
1046
1740
|
}
|
|
1047
1741
|
else {
|
|
1048
|
-
processNodeWithBuffer(node, identifier, exec);
|
|
1742
|
+
await processNodeWithBuffer(node, identifier, exec);
|
|
1049
1743
|
}
|
|
1050
1744
|
};
|
|
1051
1745
|
// recv a message
|
|
1052
|
-
ws.on('CB:message', (node) => {
|
|
1053
|
-
processNode('message', node, 'processing message', handleMessage);
|
|
1746
|
+
ws.on('CB:message', async (node) => {
|
|
1747
|
+
await processNode('message', node, 'processing message', handleMessage);
|
|
1054
1748
|
});
|
|
1055
1749
|
ws.on('CB:call', async (node) => {
|
|
1056
|
-
processNode('call', node, 'handling call', handleCall);
|
|
1750
|
+
await processNode('call', node, 'handling call', handleCall);
|
|
1057
1751
|
});
|
|
1058
|
-
ws.on('CB:receipt', node => {
|
|
1059
|
-
processNode('receipt', node, 'handling receipt', handleReceipt);
|
|
1752
|
+
ws.on('CB:receipt', async (node) => {
|
|
1753
|
+
await processNode('receipt', node, 'handling receipt', handleReceipt);
|
|
1060
1754
|
});
|
|
1061
1755
|
ws.on('CB:notification', async (node) => {
|
|
1062
|
-
processNode('notification', node, 'handling notification', handleNotification);
|
|
1756
|
+
await processNode('notification', node, 'handling notification', handleNotification);
|
|
1063
1757
|
});
|
|
1064
1758
|
ws.on('CB:ack,class:message', (node) => {
|
|
1065
|
-
handleBadAck(node)
|
|
1066
|
-
.catch(error => onUnexpectedError(error, 'handling bad ack'));
|
|
1759
|
+
handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'));
|
|
1067
1760
|
});
|
|
1068
|
-
ev.on('call', ([call]) => {
|
|
1761
|
+
ev.on('call', async ([call]) => {
|
|
1762
|
+
if (!call) {
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1069
1765
|
// missed call + group call notification message generation
|
|
1070
1766
|
if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
|
|
1071
1767
|
const msg = {
|
|
@@ -1074,37 +1770,147 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1074
1770
|
id: call.id,
|
|
1075
1771
|
fromMe: false
|
|
1076
1772
|
},
|
|
1077
|
-
messageTimestamp:
|
|
1773
|
+
messageTimestamp: unixTimestampSeconds(call.date)
|
|
1078
1774
|
};
|
|
1079
1775
|
if (call.status === 'timeout') {
|
|
1080
1776
|
if (call.isGroup) {
|
|
1081
|
-
msg.messageStubType = call.isVideo
|
|
1777
|
+
msg.messageStubType = call.isVideo
|
|
1778
|
+
? WAMessageStubType.CALL_MISSED_GROUP_VIDEO
|
|
1779
|
+
: WAMessageStubType.CALL_MISSED_GROUP_VOICE;
|
|
1082
1780
|
}
|
|
1083
1781
|
else {
|
|
1084
|
-
msg.messageStubType = call.isVideo ?
|
|
1782
|
+
msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
|
|
1085
1783
|
}
|
|
1086
1784
|
}
|
|
1087
1785
|
else {
|
|
1088
1786
|
msg.message = { call: { callKey: Buffer.from(call.id) } };
|
|
1089
1787
|
}
|
|
1090
|
-
const protoMsg =
|
|
1091
|
-
upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1788
|
+
const protoMsg = proto.WebMessageInfo.fromObject(msg);
|
|
1789
|
+
await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1092
1790
|
}
|
|
1093
1791
|
});
|
|
1094
|
-
|
|
1792
|
+
/** timestamp of last tctoken prune run — throttles to once per 24h */
|
|
1793
|
+
let lastTcTokenPruneTs = 0;
|
|
1794
|
+
/** dedupe in-flight 463 recovery token issuance by target JID */
|
|
1795
|
+
const inFlight463Recoveries = new Set();
|
|
1796
|
+
ev.on('connection.update', ({ isOnline, connection }) => {
|
|
1095
1797
|
if (typeof isOnline !== 'undefined') {
|
|
1096
1798
|
sendActiveReceipts = isOnline;
|
|
1097
1799
|
logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
|
|
1098
1800
|
}
|
|
1801
|
+
// Flush pending tctoken index save on disconnect to avoid writing after close
|
|
1802
|
+
if (connection === 'close' && tcTokenIndexTimer) {
|
|
1803
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1804
|
+
tcTokenIndexTimer = undefined;
|
|
1805
|
+
// Best-effort flush — may fail if store is already closed
|
|
1806
|
+
try {
|
|
1807
|
+
void Promise.resolve(flushTcTokenIndex()).catch(() => { });
|
|
1808
|
+
}
|
|
1809
|
+
catch {
|
|
1810
|
+
/* ignore sync errors */
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
// Prune expired tctokens when coming online, at most once per 24 hours
|
|
1814
|
+
// Matches WA Web's CLEAN_TC_TOKENS task
|
|
1815
|
+
// Note: don't gate on tcTokenKnownJids.size — the index may still be loading
|
|
1816
|
+
if (isOnline) {
|
|
1817
|
+
const now = Date.now();
|
|
1818
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
1819
|
+
if (now - lastTcTokenPruneTs >= DAY_MS) {
|
|
1820
|
+
lastTcTokenPruneTs = now;
|
|
1821
|
+
void pruneExpiredTcTokens();
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1099
1824
|
});
|
|
1825
|
+
registerSocketEndHandler(() => {
|
|
1826
|
+
if (!config.msgRetryCounterCache && msgRetryCache.close) {
|
|
1827
|
+
msgRetryCache.close();
|
|
1828
|
+
}
|
|
1829
|
+
if (!config.callOfferCache && callOfferCache.close) {
|
|
1830
|
+
callOfferCache.close();
|
|
1831
|
+
}
|
|
1832
|
+
identityAssertDebounce.close();
|
|
1833
|
+
sendActiveReceipts = false;
|
|
1834
|
+
});
|
|
1835
|
+
async function pruneExpiredTcTokens() {
|
|
1836
|
+
try {
|
|
1837
|
+
await tcTokenIndexLoaded;
|
|
1838
|
+
// Union with the persisted index picks up JIDs added by other layers
|
|
1839
|
+
// (history sync) without needing inter-module wiring.
|
|
1840
|
+
const persisted = await readTcTokenIndex(authState.keys);
|
|
1841
|
+
const allJids = new Set(tcTokenKnownJids);
|
|
1842
|
+
for (const jid of persisted)
|
|
1843
|
+
allJids.add(jid);
|
|
1844
|
+
if (!allJids.size)
|
|
1845
|
+
return;
|
|
1846
|
+
const jids = [...allJids];
|
|
1847
|
+
const allTokens = await authState.keys.get('tctoken', jids);
|
|
1848
|
+
const writes = {};
|
|
1849
|
+
const survivors = new Set();
|
|
1850
|
+
let mutated = 0;
|
|
1851
|
+
for (const jid of jids) {
|
|
1852
|
+
const entry = allTokens[jid];
|
|
1853
|
+
if (!entry) {
|
|
1854
|
+
// Tracked but nothing in store — drop from index.
|
|
1855
|
+
mutated++;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
const hasPeerToken = !!entry.token?.length;
|
|
1859
|
+
const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
|
|
1860
|
+
const hasSenderTs = entry.senderTimestamp !== undefined;
|
|
1861
|
+
const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
|
|
1862
|
+
const keepPeerToken = hasPeerToken && !peerTokenExpired;
|
|
1863
|
+
const keepSenderTs = hasSenderTs && !senderTsExpired;
|
|
1864
|
+
if (!keepPeerToken && !keepSenderTs) {
|
|
1865
|
+
writes[jid] = null;
|
|
1866
|
+
mutated++;
|
|
1867
|
+
}
|
|
1868
|
+
else if (peerTokenExpired && keepSenderTs) {
|
|
1869
|
+
writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
|
|
1870
|
+
survivors.add(jid);
|
|
1871
|
+
mutated++;
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
survivors.add(jid);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (mutated === 0)
|
|
1878
|
+
return;
|
|
1879
|
+
await authState.keys.set({
|
|
1880
|
+
tctoken: {
|
|
1881
|
+
...writes,
|
|
1882
|
+
[TC_TOKEN_INDEX_KEY]: {
|
|
1883
|
+
token: Buffer.from(JSON.stringify([...survivors]))
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
tcTokenKnownJids.clear();
|
|
1888
|
+
for (const jid of survivors)
|
|
1889
|
+
tcTokenKnownJids.add(jid);
|
|
1890
|
+
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
|
|
1891
|
+
}
|
|
1892
|
+
catch (err) {
|
|
1893
|
+
logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1100
1896
|
return {
|
|
1101
1897
|
...sock,
|
|
1102
1898
|
sendMessageAck,
|
|
1103
1899
|
sendRetryRequest,
|
|
1104
1900
|
rejectCall,
|
|
1105
|
-
offerCall,
|
|
1106
1901
|
fetchMessageHistory,
|
|
1107
1902
|
requestPlaceholderResend,
|
|
1903
|
+
messageRetryManager,
|
|
1904
|
+
sendText,
|
|
1905
|
+
sendImage,
|
|
1906
|
+
sendVideo,
|
|
1907
|
+
sendAudio,
|
|
1908
|
+
sendDocument,
|
|
1909
|
+
sendLocation,
|
|
1910
|
+
sendPoll,
|
|
1911
|
+
sendQuiz,
|
|
1912
|
+
sendPtv,
|
|
1913
|
+
statusMention
|
|
1108
1914
|
};
|
|
1109
1915
|
};
|
|
1110
|
-
|
|
1916
|
+
//# sourceMappingURL=messages-recv.js.map
|