whatsapp-store-db 1.3.67 → 1.3.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-store-db",
3
- "version": "1.3.67",
3
+ "version": "1.3.69",
4
4
  "description": "Minimal Baileys data storage for your favorite DBMS built with Prisma",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "typescript": "^4.9.3"
31
31
  },
32
32
  "dependencies": {
33
- "baileys": "7.0.0-rc.9",
33
+ "baileys": "7.0.0-rc13",
34
34
  "patch-package": "^8.0.0",
35
35
  "tiny-invariant": "^1.3.1"
36
36
  },
@@ -0,0 +1,294 @@
1
+ --- a/node_modules/baileys/lib/Socket/messages-recv.js
2
+ +++ b/node_modules/baileys/lib/Socket/messages-recv.js
3
+ @@ -54,11 +54,12 @@
4
+ return sendPeerDataOperationMessage(pdoMessage);
5
+ };
6
+ const requestPlaceholderResend = async (messageKey, msgData) => {
7
+ + const sessionJid = authState.creds.me?.id || 'unknown';
8
+ if (!authState.creds.me?.id) {
9
+ throw new Boom('Not authenticated');
10
+ }
11
+ if (await placeholderResendCache.get(messageKey?.id)) {
12
+ - logger.debug({ messageKey }, 'already requested resend');
13
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Placeholder resend already requested (dedup), skipping');
14
+ return;
15
+ }
16
+ else {
17
+ @@ -66,9 +67,10 @@
18
+ // metadata (LID details, timestamps, etc.) that the phone may omit
19
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
20
+ }
21
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Waiting 2s before sending PDO request');
22
+ await delay(2000);
23
+ if (!(await placeholderResendCache.get(messageKey?.id))) {
24
+ - logger.debug({ messageKey }, 'message received while resend requested');
25
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Message arrived during 2s wait, no PDO needed');
26
+ return 'RESOLVED';
27
+ }
28
+ const pdoMessage = {
29
+ @@ -79,13 +81,43 @@
30
+ ],
31
+ peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
32
+ };
33
+ - setTimeout(async () => {
34
+ - if (await placeholderResendCache.get(messageKey?.id)) {
35
+ - logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
36
+ - await placeholderResendCache.del(messageKey?.id);
37
+ + // PDO retry loop — up to 3 attempts, 15s wait each (~47s total with 2s initial delay)
38
+ + const MAX_PDO_ATTEMPTS = 3;
39
+ + let stanzaId;
40
+ + for (let attempt = 1; attempt <= MAX_PDO_ATTEMPTS; attempt++) {
41
+ + try {
42
+ + const currentStanzaId = await sendPeerDataOperationMessage(pdoMessage);
43
+ + if (attempt === 1)
44
+ + stanzaId = currentStanzaId;
45
+ + // Store mapping: stanzaId → messageKey.id so PDO response can clear the right cache entry
46
+ + if (currentStanzaId) {
47
+ + await placeholderResendCache.set(`pdo_${currentStanzaId}`, messageKey?.id);
48
+ + }
49
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid, attempt, maxAttempts: MAX_PDO_ATTEMPTS }, 'CTWA: Placeholder resend PDO request sent');
50
+ + // Wait 15s for response
51
+ + await delay(15000);
52
+ + // Check if response arrived (cache entry cleared by process-message.js handler)
53
+ + if (!(await placeholderResendCache.get(messageKey?.id))) {
54
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid, attempt }, 'CTWA_TRACE: PDO response received within 15s');
55
+ + return stanzaId;
56
+ + }
57
+ + // No response yet
58
+ + if (attempt < MAX_PDO_ATTEMPTS) {
59
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid, attempt, maxAttempts: MAX_PDO_ATTEMPTS }, 'CTWA: PDO no response after 15s, retrying...');
60
+ + }
61
+ }
62
+ - }, 8000);
63
+ - return sendPeerDataOperationMessage(pdoMessage);
64
+ + catch (err) {
65
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, error: err, attempt }, 'CTWA: PDO request failed');
66
+ + if (attempt >= MAX_PDO_ATTEMPTS) {
67
+ + break;
68
+ + }
69
+ + }
70
+ + }
71
+ + // All attempts exhausted — message lost
72
+ + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid, attempts: MAX_PDO_ATTEMPTS }, 'CTWA: PDO no response after all retries - phone possibly offline, message LOST');
73
+ + ev.emit('ctwa.failure', { messageKey, msgData, session: sessionJid });
74
+ + await placeholderResendCache.del(messageKey?.id);
75
+ + return stanzaId;
76
+ };
77
+ const handleMexNotification = async (node) => {
78
+ const updateNode = getBinaryNodeChild(node, 'update');
79
+ @@ -1296,68 +1328,104 @@
80
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
81
+ }
82
+ // message failed to decrypt
83
+ - if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
84
+ - if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
85
+ - acked = true;
86
+ - return sendMessageAck(node, NACK_REASONS.ParsingError);
87
+ - }
88
+ - if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
89
+ - // Message arrived without encryption (e.g. CTWA ads messages).
90
+ - // Check if this is eligible for placeholder resend (matching WA Web filters).
91
+ + if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
92
+ + const sessionJid = authState.creds.me?.id || 'unknown';
93
+ + const stubParam = msg?.messageStubParameters?.[0] || '';
94
+ + logger.error({ session: sessionJid, msgId: msg.key?.id, remoteJid: msg.key?.remoteJid, fromMe: msg.key?.fromMe, stubParam, category, msgCategory: msg.category }, 'CTWA_TRACE: CIPHERTEXT stub received');
95
+ + // CTWA placeholder recovery — handle BEFORE category gate.
96
+ + // "Message absent from node" means the message content was never sent to this
97
+ + // linked device (common for CTWA ad messages). We must attempt PDO recovery
98
+ + // regardless of message category, because CTWA messages may arrive with any
99
+ + // category value including 'peer'.
100
+ + if (stubParam === NO_MESSAGE_FOUND_ERROR_TEXT) {
101
+ + // Skip newsletter/channel messages — these are not CTWA ads
102
+ + if (isJidNewsletter(msg.key?.remoteJid || node.attrs.from || '')) {
103
+ + logger.error({ session: sessionJid, msgId: msg.key?.id, remoteJid: msg.key?.remoteJid }, 'CTWA_TRACE: Skipping placeholder resend for newsletter/channel message');
104
+ + acked = true;
105
+ + return sendMessageAck(node);
106
+ + }
107
+ const unavailableNode = getBinaryNodeChild(node, 'unavailable');
108
+ const unavailableType = unavailableNode?.attrs?.type;
109
+ if (unavailableType === 'bot_unavailable_fanout' ||
110
+ unavailableType === 'hosted_unavailable_fanout' ||
111
+ unavailableType === 'view_once_unavailable_fanout') {
112
+ - logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
113
+ + logger.error({ session: sessionJid, msgId: msg.key?.id, unavailableType }, 'CTWA_TRACE: Skipping placeholder resend for excluded unavailable type');
114
+ acked = true;
115
+ return sendMessageAck(node);
116
+ }
117
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
118
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
119
+ - logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
120
+ + logger.error({ session: sessionJid, msgId: msg.key?.id, messageAge }, 'CTWA_TRACE: Skipping placeholder resend for old message (>3 days)');
121
+ acked = true;
122
+ return sendMessageAck(node);
123
+ }
124
+ - // Request the real content from the phone via placeholder resend PDO.
125
+ - // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
126
+ - // and store the requestId in stubParameters[1] so users can correlate
127
+ - // with the incoming PDO response event.
128
+ + logger.error({ session: sessionJid, msgId: msg.key?.id, remoteJid: msg.key?.remoteJid, fromMe: msg.key?.fromMe, category, msgCategory: msg.category }, 'CTWA: Message absent from node detected, requesting placeholder resend from phone');
129
+ + // Build clean key and cache original metadata so PDO response handler
130
+ + // can preserve LID details, timestamps, pushName, etc.
131
+ const cleanKey = {
132
+ remoteJid: msg.key.remoteJid,
133
+ fromMe: msg.key.fromMe,
134
+ id: msg.key.id,
135
+ participant: msg.key.participant
136
+ };
137
+ - // Cache the original message metadata so the PDO response handler
138
+ - // can preserve key fields (LID details etc.) that the phone may omit
139
+ + // Extract CTWA ad metadata from raw node for fallback message construction
140
+ + // (used by src/wa.ts ctwa.failure handler when PDO never recovers content)
141
+ + const senderPn = node.attrs?.sender_pn || '';
142
+ + const urlTextNode = getBinaryNodeChild(node, 'url_text');
143
+ + const urlNumberNode = getBinaryNodeChild(node, 'url_number');
144
+ + const ctwaUrlText = urlTextNode?.content instanceof Uint8Array
145
+ + ? Buffer.from(urlTextNode.content).toString('utf-8')
146
+ + : (typeof urlTextNode?.content === 'string' ? urlTextNode.content : '');
147
+ + const ctwaUrlNumber = urlNumberNode?.content instanceof Uint8Array
148
+ + ? Buffer.from(urlNumberNode.content).toString('utf-8')
149
+ + : (typeof urlNumberNode?.content === 'string' ? urlNumberNode.content : '');
150
+ + const reportingNode = getBinaryNodeChild(node, 'reporting');
151
+ + const reportingTag = reportingNode?.attrs?.reporting_tag || '';
152
+ const msgData = {
153
+ key: msg.key,
154
+ messageTimestamp: msg.messageTimestamp,
155
+ pushName: msg.pushName,
156
+ participant: msg.participant,
157
+ - verifiedBizName: msg.verifiedBizName
158
+ + verifiedBizName: msg.verifiedBizName,
159
+ + ctwaContext: {
160
+ + senderPn: senderPn,
161
+ + urlText: ctwaUrlText,
162
+ + urlNumber: ctwaUrlNumber,
163
+ + reportingTag: reportingTag
164
+ + }
165
+ };
166
+ requestPlaceholderResend(cleanKey, msgData)
167
+ - .then(requestId => {
168
+ - if (requestId && requestId !== 'RESOLVED') {
169
+ - logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
170
+ + .then(result => {
171
+ + if (result === 'RESOLVED') {
172
+ + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'CTWA: Message received during resend delay');
173
+ + }
174
+ + else if (result) {
175
+ + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid, requestId: result }, 'CTWA: Placeholder resend PDO request sent');
176
+ ev.emit('messages.update', [
177
+ {
178
+ key: msg.key,
179
+ - update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
180
+ + update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, result] }
181
+ }
182
+ ]);
183
+ }
184
+ + else {
185
+ + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'CTWA: Placeholder resend skipped (already requested or dedup)');
186
+ + }
187
+ })
188
+ - .catch(err => {
189
+ - logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
190
+ + .catch(error => {
191
+ + logger.error({ session: sessionJid, error, msgId: msg.key.id }, 'CTWA: Failed to request placeholder resend');
192
+ });
193
+ acked = true;
194
+ await sendMessageAck(node);
195
+ // Don't return — fall through to upsertMessage so the stub is emitted
196
+ }
197
+ - else {
198
+ + else if (msg.category !== 'peer') {
199
+ + // For all other CIPHERTEXT errors, apply the category gate
200
+ + // (peer category messages use a different retry mechanism)
201
+ + if (stubParam === MISSING_KEYS_ERROR_TEXT) {
202
+ + acked = true;
203
+ + return sendMessageAck(node, NACK_REASONS.ParsingError);
204
+ + }
205
+ // Skip retry for expired status messages (>24h old)
206
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
207
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
208
+ --- a/node_modules/baileys/lib/Utils/process-message.js
209
+ +++ b/node_modules/baileys/lib/Utils/process-message.js
210
+ @@ -333,6 +333,9 @@
211
+ //eslint-disable-next-line max-depth
212
+ if (msgId) {
213
+ await placeholderResendCache?.del(msgId);
214
+ + // Also clean up pdo_ mapping entries written by requestPlaceholderResend
215
+ + await placeholderResendCache?.del(`pdo_${response.stanzaId}`);
216
+ + logger?.error({ session: meId, stanzaId: response.stanzaId, msgId }, 'CTWA_TRACE: Cleared cache for PDO response');
217
+ }
218
+ let finalMsg;
219
+ //eslint-disable-next-line max-depth
220
+ @@ -344,11 +347,53 @@
221
+ cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
222
+ }
223
+ finalMsg = cachedData;
224
+ + logger?.error({ session: meId, msgId }, 'CTWA_TRACE: Merged PDO content with cached metadata');
225
+ }
226
+ else {
227
+ finalMsg = webMessageInfo;
228
+ }
229
+ - logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend');
230
+ + // Apply LID resolution and JID normalization so recovered messages
231
+ + // surface under their phone-number JID (matches src/wa.ts expectations)
232
+ + //eslint-disable-next-line max-depth
233
+ + if (finalMsg.key?.remoteJid) {
234
+ + const rawJid = finalMsg.key.remoteJid;
235
+ + //eslint-disable-next-line max-depth
236
+ + if (isLidUser(rawJid)) {
237
+ + try {
238
+ + const pn = await signalRepository.lidMapping.getPNForLID(rawJid);
239
+ + //eslint-disable-next-line max-depth
240
+ + if (pn) {
241
+ + const phoneJid = jidNormalizedUser(pn.includes('@') ? pn : `${pn}@s.whatsapp.net`);
242
+ + logger?.error({ session: meId, lid: rawJid, resolved: phoneJid }, 'CTWA: Resolved LID to phone number for recovered message');
243
+ + finalMsg.key.remoteJidAlt = rawJid;
244
+ + finalMsg.key.remoteJid = phoneJid;
245
+ + }
246
+ + else {
247
+ + logger?.error({ session: meId, lid: rawJid }, 'CTWA: Could not resolve LID to phone number - no mapping found');
248
+ + }
249
+ + }
250
+ + catch (e) {
251
+ + logger?.error({ session: meId, lid: rawJid, error: e }, 'CTWA: Error resolving LID to phone number');
252
+ + }
253
+ + }
254
+ + else {
255
+ + const normalized = jidNormalizedUser(rawJid);
256
+ + //eslint-disable-next-line max-depth
257
+ + if (normalized !== rawJid) {
258
+ + logger?.error({ session: meId, rawJid, normalized }, 'CTWA: Normalized remoteJid (stripped device suffix)');
259
+ + finalMsg.key.remoteJid = normalized;
260
+ + }
261
+ + }
262
+ + }
263
+ + //eslint-disable-next-line max-depth
264
+ + if (finalMsg.key?.participant) {
265
+ + const normalizedParticipant = jidNormalizedUser(finalMsg.key.participant);
266
+ + //eslint-disable-next-line max-depth
267
+ + if (normalizedParticipant !== finalMsg.key.participant) {
268
+ + finalMsg.key.participant = normalizedParticipant;
269
+ + }
270
+ + }
271
+ + logger?.error({ session: meId, msgId: finalMsg.key?.id, remoteJid: finalMsg.key?.remoteJid, requestId: response.stanzaId }, 'CTWA: Message recovered from phone via PDO');
272
+ ev.emit('messages.upsert', {
273
+ messages: [finalMsg],
274
+ type: 'notify',
275
+ @@ -356,7 +401,7 @@
276
+ });
277
+ }
278
+ catch (err) {
279
+ - logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response');
280
+ + logger?.error({ session: meId, err, stanzaId: response.stanzaId }, 'CTWA: Failed to decode placeholder resend response');
281
+ }
282
+ }
283
+ }
284
+ --- a/node_modules/baileys/lib/Utils/validate-connection.js
285
+ +++ b/node_modules/baileys/lib/Utils/validate-connection.js
286
+ @@ -13,7 +13,7 @@
287
+ secondary: config.version[1],
288
+ tertiary: config.version[2]
289
+ },
290
+ - platform: proto.ClientPayload.UserAgent.Platform.WEB,
291
+ + platform: proto.ClientPayload.UserAgent.Platform.MACOS,
292
+ releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
293
+ osVersion: '0.1',
294
+ device: 'Desktop',
@@ -1,389 +0,0 @@
1
- diff --git a/node_modules/baileys/lib/Socket/messages-recv.js b/node_modules/baileys/lib/Socket/messages-recv.js
2
- index f2e2e10..1535b62 100644
3
- --- a/node_modules/baileys/lib/Socket/messages-recv.js
4
- +++ b/node_modules/baileys/lib/Socket/messages-recv.js
5
- @@ -50,20 +50,24 @@ export const makeMessagesRecvSocket = (config) => {
6
- };
7
- return sendPeerDataOperationMessage(pdoMessage);
8
- };
9
- - const requestPlaceholderResend = async (messageKey) => {
10
- + const requestPlaceholderResend = async (messageKey, msgData) => {
11
- + const sessionJid = authState.creds.me?.id || 'unknown';
12
- if (!authState.creds.me?.id) {
13
- throw new Boom('Not authenticated');
14
- }
15
- if (placeholderResendCache.get(messageKey?.id)) {
16
- - logger.debug({ messageKey }, 'already requested resend');
17
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Placeholder resend already requested (dedup), skipping');
18
- return;
19
- }
20
- else {
21
- - await placeholderResendCache.set(messageKey?.id, true);
22
- + // Store original message data so PDO response handler can preserve
23
- + // metadata (LID details, timestamps, pushName, etc.) that the phone may omit
24
- + await placeholderResendCache.set(messageKey?.id, msgData || true);
25
- }
26
- - await delay(5000);
27
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Waiting 2s before sending PDO request');
28
- + await delay(2000);
29
- if (!placeholderResendCache.get(messageKey?.id)) {
30
- - logger.debug({ messageKey }, 'message received while resend requested');
31
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: Message arrived during 5s wait, no PDO needed');
32
- return 'RESOLVED';
33
- }
34
- const pdoMessage = {
35
- @@ -74,13 +78,51 @@ export const makeMessagesRecvSocket = (config) => {
36
- ],
37
- peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
38
- };
39
- - setTimeout(async () => {
40
- - if (placeholderResendCache.get(messageKey?.id)) {
41
- - logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
42
- - await placeholderResendCache.del(messageKey?.id);
43
- - }
44
- - }, 15000);
45
- - return sendPeerDataOperationMessage(pdoMessage);
46
- + // Send first PDO attempt
47
- + const stanzaId = await sendPeerDataOperationMessage(pdoMessage);
48
- + // Store mapping: stanzaId → messageKey.id so PDO response can clear the right cache entry
49
- + if (stanzaId) {
50
- + await placeholderResendCache.set(`pdo_${stanzaId}`, messageKey?.id);
51
- + }
52
- + // Wait 15s for response, then retry once if no response
53
- + await new Promise((resolve) => {
54
- + setTimeout(async () => {
55
- + if (!placeholderResendCache.get(messageKey?.id)) {
56
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: PDO response received within 15s (first attempt)');
57
- + resolve(undefined);
58
- + return;
59
- + }
60
- + // First attempt failed — retry once
61
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA: PDO no response after 15s, retrying once...');
62
- + try {
63
- + const retryStanzaId = await sendPeerDataOperationMessage(pdoMessage);
64
- + if (retryStanzaId) {
65
- + await placeholderResendCache.set(`pdo_${retryStanzaId}`, messageKey?.id);
66
- + }
67
- + // Wait another 15s for retry response
68
- + setTimeout(async () => {
69
- + if (placeholderResendCache.get(messageKey?.id)) {
70
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA: PDO no response after retry - phone possibly offline, message LOST');
71
- + // Emit failure event so wa.ts can notify backend
72
- + ev.emit('ctwa.failure', { messageKey, session: sessionJid });
73
- + await placeholderResendCache.del(messageKey?.id);
74
- + }
75
- + else {
76
- + logger.error({ session: sessionJid, msgId: messageKey?.id, remoteJid: messageKey?.remoteJid }, 'CTWA_TRACE: PDO response received within 15s (retry attempt)');
77
- + }
78
- + resolve(undefined);
79
- + }, 15000);
80
- + }
81
- + catch (retryErr) {
82
- + logger.error({ session: sessionJid, msgId: messageKey?.id, error: retryErr }, 'CTWA: PDO retry request failed');
83
- + // Emit failure event
84
- + ev.emit('ctwa.failure', { messageKey, session: sessionJid });
85
- + await placeholderResendCache.del(messageKey?.id);
86
- + resolve(undefined);
87
- + }
88
- + }, 15000);
89
- + });
90
- + return stanzaId;
91
- };
92
- // Handles mex newsletter notifications
93
- const handleMexNewsletterNotification = async (node) => {
94
- @@ -987,53 +1029,125 @@ export const makeMessagesRecvSocket = (config) => {
95
- await processingMutex.mutex(async () => {
96
- await decrypt();
97
- // message failed to decrypt
98
- - if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
99
- - if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT ||
100
- - msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
101
- - return sendMessageAck(node);
102
- + if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
103
- + const sessionJid = authState.creds.me?.id || 'unknown';
104
- + const stubParam = msg?.messageStubParameters?.[0] || '';
105
- + logger.error({ session: sessionJid, msgId: msg.key?.id, remoteJid: msg.key?.remoteJid, fromMe: msg.key?.fromMe, stubParam, category, msgCategory: msg.category }, 'CTWA_TRACE: CIPHERTEXT stub received');
106
- + // CTWA placeholder recovery — handle BEFORE category gate
107
- + // "Message absent from node" means the message content was never sent to this
108
- + // linked device (common for CTWA ad messages). We must attempt PDO recovery
109
- + // regardless of message category, because CTWA messages may arrive with any
110
- + // category value including 'peer'.
111
- + if (stubParam === NO_MESSAGE_FOUND_ERROR_TEXT) {
112
- + // Filter out unavailable types that shouldn't trigger PDO (from official fix)
113
- + const unavailableNode = getBinaryNodeChild(node, 'unavailable');
114
- + const unavailableType = unavailableNode?.attrs?.type;
115
- + if (
116
- + unavailableType === 'bot_unavailable_fanout' ||
117
- + unavailableType === 'hosted_unavailable_fanout' ||
118
- + unavailableType === 'view_once_unavailable_fanout'
119
- + ) {
120
- + logger.error({ session: sessionJid, msgId: msg.key?.id, unavailableType }, 'CTWA_TRACE: Skipping placeholder resend for excluded unavailable type');
121
- + return sendMessageAck(node);
122
- + }
123
- + // Skip PDO for messages older than 14 days (WA Web enforces this limit)
124
- + const PLACEHOLDER_MAX_AGE_SECONDS = 14 * 24 * 60 * 60;
125
- + const messageAge = unixTimestampSeconds() - (typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : Number(msg.messageTimestamp || 0));
126
- + if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
127
- + logger.error({ session: sessionJid, msgId: msg.key?.id, messageAge }, 'CTWA_TRACE: Skipping placeholder resend for old message (>14 days)');
128
- + return sendMessageAck(node);
129
- + }
130
- + if (msg.key) {
131
- + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, category, msgCategory: msg.category }, 'CTWA: Message absent from node detected, requesting placeholder resend from phone');
132
- + // Build clean key and cache original metadata (from official fix)
133
- + // so PDO response handler can preserve LID details, timestamps, etc.
134
- + const cleanKey = {
135
- + remoteJid: msg.key.remoteJid,
136
- + fromMe: msg.key.fromMe,
137
- + id: msg.key.id,
138
- + participant: msg.key.participant
139
- + };
140
- + const msgData = {
141
- + key: msg.key,
142
- + messageTimestamp: msg.messageTimestamp,
143
- + pushName: msg.pushName,
144
- + participant: msg.participant,
145
- + verifiedBizName: msg.verifiedBizName
146
- + };
147
- + requestPlaceholderResend(cleanKey, msgData)
148
- + .then((result) => {
149
- + if (result === 'RESOLVED') {
150
- + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'CTWA: Message received during resend delay');
151
- + }
152
- + else if (result) {
153
- + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid, requestId: result }, 'CTWA: Placeholder resend PDO request sent');
154
- + // Store requestId in stubParameters for correlation (from official fix)
155
- + ev.emit('messages.update', [{
156
- + key: msg.key,
157
- + update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, result] }
158
- + }]);
159
- + }
160
- + else {
161
- + logger.error({ session: sessionJid, msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'CTWA: Placeholder resend skipped (already requested or dedup)');
162
- + }
163
- + })
164
- + .catch((error) => {
165
- + logger.error({ session: sessionJid, error, msgId: msg.key.id }, 'CTWA: Failed to request placeholder resend');
166
- + });
167
- + }
168
- + // ACK but DON'T return — fall through to upsertMessage so the
169
- + // CIPHERTEXT stub is emitted as a placeholder (from official fix)
170
- + await sendMessageAck(node);
171
- }
172
- - const errorMessage = msg?.messageStubParameters?.[0] || '';
173
- - const isPreKeyError = errorMessage.includes('PreKey');
174
- - logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
175
- - // Handle both pre-key and normal retries in single mutex
176
- - await retryMutex.mutex(async () => {
177
- - try {
178
- - if (!ws.isOpen) {
179
- - logger.debug({ node }, 'Connection closed, skipping retry');
180
- - return;
181
- - }
182
- - // Handle pre-key errors with upload and delay
183
- - if (isPreKeyError) {
184
- - logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
185
- - try {
186
- - logger.debug('Uploading pre-keys for error recovery');
187
- - await uploadPreKeys(5);
188
- - logger.debug('Waiting for server to process new pre-keys');
189
- - await delay(1000);
190
- - }
191
- - catch (uploadErr) {
192
- - logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
193
- - }
194
- - }
195
- - const encNode = getBinaryNodeChild(node, 'enc');
196
- - await sendRetryRequest(node, !encNode);
197
- - if (retryRequestDelayMs) {
198
- - await delay(retryRequestDelayMs);
199
- - }
200
- + // For all other CIPHERTEXT errors, apply the category gate
201
- + // (peer category messages use a different retry mechanism)
202
- + else if (msg.category !== 'peer') {
203
- + if (stubParam === MISSING_KEYS_ERROR_TEXT) {
204
- + return sendMessageAck(node);
205
- }
206
- - catch (err) {
207
- - logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
208
- - // Still attempt retry even if pre-key upload failed
209
- + const errorMessage = msg?.messageStubParameters?.[0] || '';
210
- + const isPreKeyError = errorMessage.includes('PreKey');
211
- + logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
212
- + // Handle both pre-key and normal retries in single mutex
213
- + await retryMutex.mutex(async () => {
214
- try {
215
- + if (!ws.isOpen) {
216
- + logger.debug({ node }, 'Connection closed, skipping retry');
217
- + return;
218
- + }
219
- + // Handle pre-key errors with upload and delay
220
- + if (isPreKeyError) {
221
- + logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
222
- + try {
223
- + logger.debug('Uploading pre-keys for error recovery');
224
- + await uploadPreKeys(5);
225
- + logger.debug('Waiting for server to process new pre-keys');
226
- + await delay(1000);
227
- + }
228
- + catch (uploadErr) {
229
- + logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
230
- + }
231
- + }
232
- const encNode = getBinaryNodeChild(node, 'enc');
233
- await sendRetryRequest(node, !encNode);
234
- + if (retryRequestDelayMs) {
235
- + await delay(retryRequestDelayMs);
236
- + }
237
- }
238
- - catch (retryErr) {
239
- - logger.error({ retryErr }, 'Failed to send retry after error handling');
240
- + catch (err) {
241
- + logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
242
- + // Still attempt retry even if pre-key upload failed
243
- + try {
244
- + const encNode = getBinaryNodeChild(node, 'enc');
245
- + await sendRetryRequest(node, !encNode);
246
- + }
247
- + catch (retryErr) {
248
- + logger.error({ retryErr }, 'Failed to send retry after error handling');
249
- + }
250
- }
251
- - }
252
- - await sendMessageAck(node, NACK_REASONS.UnhandledError);
253
- - });
254
- + await sendMessageAck(node, NACK_REASONS.UnhandledError);
255
- + });
256
- + }
257
- }
258
- else {
259
- const isNewsletter = isJidNewsletter(msg.key.remoteJid);
260
- diff --git a/node_modules/baileys/lib/Utils/process-message.js b/node_modules/baileys/lib/Utils/process-message.js
261
- index 7927483..7111219 100644
262
- --- a/node_modules/baileys/lib/Utils/process-message.js
263
- +++ b/node_modules/baileys/lib/Utils/process-message.js
264
- @@ -215,23 +215,99 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
265
- case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
266
- const response = protocolMsg.peerDataOperationRequestResponseMessage;
267
- if (response) {
268
- - await placeholderResendCache?.del(response.stanzaId);
269
- // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
270
- - const { peerDataOperationResult } = response;
271
- + const peerDataOperationResult = response.peerDataOperationResult || [];
272
- for (const result of peerDataOperationResult) {
273
- - const { placeholderMessageResendResponse: retryResponse } = result;
274
- + const retryResponse = result?.placeholderMessageResendResponse;
275
- //eslint-disable-next-line max-depth
276
- - if (retryResponse) {
277
- + if (!retryResponse?.webMessageInfoBytes) {
278
- + continue;
279
- + }
280
- + //eslint-disable-next-line max-depth
281
- + try {
282
- const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
283
- - // wait till another upsert event is available, don't want it to be part of the PDO response message
284
- - // TODO: parse through proper message handling utilities (to add relevant key fields)
285
- - setTimeout(() => {
286
- - ev.emit('messages.upsert', {
287
- - messages: [webMessageInfo],
288
- - type: 'notify',
289
- - requestId: response.stanzaId
290
- - });
291
- - }, 500);
292
- + const msgId = webMessageInfo.key?.id;
293
- + // Retrieve cached original message data (from official fix)
294
- + // Preserves LID details, timestamps, pushName, etc. that the phone may omit
295
- + const cachedData = msgId ? placeholderResendCache?.get(msgId) : undefined;
296
- + //eslint-disable-next-line max-depth
297
- + if (msgId) {
298
- + await placeholderResendCache?.del(msgId);
299
- + // Also clean up pdo_ mapping entries
300
- + await placeholderResendCache?.del(`pdo_${response.stanzaId}`);
301
- + logger.error({ session: meId, stanzaId: response.stanzaId, msgId }, 'CTWA_TRACE: Cleared cache for PDO response');
302
- + }
303
- + let finalMsg;
304
- + //eslint-disable-next-line max-depth
305
- + if (cachedData && typeof cachedData === 'object') {
306
- + // Apply decoded message content onto cached metadata (from official fix)
307
- + // This preserves LID details, timestamps, pushName, etc.
308
- + cachedData.message = webMessageInfo.message;
309
- + if (webMessageInfo.messageTimestamp) {
310
- + cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
311
- + }
312
- + finalMsg = cachedData;
313
- + logger.error({ session: meId, msgId }, 'CTWA_TRACE: Merged PDO content with cached metadata');
314
- + }
315
- + else {
316
- + finalMsg = webMessageInfo;
317
- + }
318
- + // Apply LID resolution and JID normalization (our enhancement)
319
- + if (finalMsg.key?.remoteJid) {
320
- + const rawJid = finalMsg.key.remoteJid;
321
- + if (isLidUser(rawJid)) {
322
- + try {
323
- + const pn = await signalRepository.lidMapping.getPNForLID(rawJid);
324
- + if (pn) {
325
- + const phoneJid = jidNormalizedUser(pn.includes('@') ? pn : `${pn}@s.whatsapp.net`);
326
- + logger.error({ session: meId, lid: rawJid, resolved: phoneJid }, 'CTWA: Resolved LID to phone number for recovered message');
327
- + finalMsg.key.remoteJidAlt = rawJid;
328
- + finalMsg.key.remoteJid = phoneJid;
329
- + }
330
- + else {
331
- + logger.error({ session: meId, lid: rawJid }, 'CTWA: Could not resolve LID to phone number - no mapping found');
332
- + }
333
- + }
334
- + catch (e) {
335
- + logger.error({ session: meId, lid: rawJid, error: e }, 'CTWA: Error resolving LID to phone number');
336
- + }
337
- + }
338
- + else {
339
- + const normalized = jidNormalizedUser(rawJid);
340
- + if (normalized !== rawJid) {
341
- + logger.error({ session: meId, rawJid, normalized }, 'CTWA: Normalized remoteJid (stripped device suffix)');
342
- + finalMsg.key.remoteJid = normalized;
343
- + }
344
- + }
345
- + }
346
- + // Normalize participant JID if present
347
- + if (finalMsg.key?.participant) {
348
- + const normalizedParticipant = jidNormalizedUser(finalMsg.key.participant);
349
- + if (normalizedParticipant !== finalMsg.key.participant) {
350
- + finalMsg.key.participant = normalizedParticipant;
351
- + }
352
- + }
353
- + const recoveredContent = finalMsg.message?.conversation
354
- + || finalMsg.message?.extendedTextMessage?.text
355
- + || finalMsg.message?.imageMessage?.caption
356
- + || finalMsg.message?.videoMessage?.caption
357
- + || (finalMsg.message?.imageMessage ? '[image]' : '')
358
- + || (finalMsg.message?.videoMessage ? '[video]' : '')
359
- + || (finalMsg.message?.audioMessage ? '[audio]' : '')
360
- + || (finalMsg.message?.documentMessage ? '[document]' : '')
361
- + || (finalMsg.message?.stickerMessage ? '[sticker]' : '')
362
- + || (finalMsg.message?.contactMessage ? '[contact]' : '')
363
- + || (finalMsg.message?.locationMessage ? '[location]' : '')
364
- + || '[unknown type]';
365
- + logger.error({ session: meId, msgId: finalMsg.key?.id, remoteJid: finalMsg.key?.remoteJid, recoveredContent, requestId: response.stanzaId }, 'CTWA: Message recovered from phone via PDO');
366
- + ev.emit('messages.upsert', {
367
- + messages: [finalMsg],
368
- + type: 'notify',
369
- + requestId: response.stanzaId
370
- + });
371
- + }
372
- + catch (err) {
373
- + logger.error({ session: meId, err, stanzaId: response.stanzaId }, 'CTWA: Failed to decode placeholder resend response');
374
- }
375
- }
376
- }
377
- diff --git a/node_modules/baileys/lib/Utils/validate-connection.js b/node_modules/baileys/lib/Utils/validate-connection.js
378
- index 42fb902..38a2e0c 100644
379
- --- a/node_modules/baileys/lib/Utils/validate-connection.js
380
- +++ b/node_modules/baileys/lib/Utils/validate-connection.js
381
- @@ -13,7 +13,7 @@ const getUserAgent = (config) => {
382
- secondary: config.version[1],
383
- tertiary: config.version[2]
384
- },
385
- - platform: proto.ClientPayload.UserAgent.Platform.WEB,
386
- + platform: proto.ClientPayload.UserAgent.Platform.MACOS,
387
- releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
388
- osVersion: '0.1',
389
- device: 'Desktop',