@pontalabs/baileys 1.0.6 → 1.0.7

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