@nexustechpro/baileys 2.1.2 → 2.1.6

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.
@@ -1,179 +1,200 @@
1
1
  import { proto } from '../../WAProto/index.js';
2
- import { Boom } from '@hapi/boom'
2
+ import { Boom } from '@hapi/boom';
3
3
  import { WAMessageStubType } from '../Types/index.js';
4
4
  import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
5
- import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
5
+ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
6
6
  import { aesDecryptGCM, hmacSign } from './crypto.js';
7
- import { resolveTcTokenJid, buildMergedTcTokenIndexWrite } from './tc-token-utils.js'
8
- import { toNumber } from './generics.js'
7
+ import { getKeyAuthor, toNumber } from './generics.js';
9
8
  import { downloadAndProcessHistorySyncNotification } from './history.js';
9
+ import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
10
+
10
11
  const REAL_MSG_STUB_TYPES = new Set([
11
12
  WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
12
13
  WAMessageStubType.CALL_MISSED_GROUP_VOICE,
13
14
  WAMessageStubType.CALL_MISSED_VIDEO,
14
- WAMessageStubType.CALL_MISSED_VOICE
15
+ WAMessageStubType.CALL_MISSED_VOICE,
15
16
  ]);
17
+
16
18
  const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
17
- /** Cleans a received message to further processing */
19
+
20
+ // Protocol message types that can only legitimately arrive from our own device.
21
+ // If they arrive from any other sender we drop them to prevent local-state spoofing.
22
+ // Cross-user types (REVOKE, MESSAGE_EDIT, EPHEMERAL_SETTING, GROUP_MEMBER_LABEL_CHANGE)
23
+ // must NOT be listed here — they legitimately arrive from other participants.
24
+ // Reference: https://github.com/tulir/whatsmeow/blob/8d3700152a/message.go#L842-L845
25
+ const SELF_ONLY_PROTOCOL_TYPES = new Set([
26
+ proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
27
+ proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
28
+ proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC,
29
+ proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE,
30
+ ]);
31
+
32
+ /** Persist tc-tokens discovered inside a history-sync payload, skipping stale entries. */
33
+ async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
34
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
35
+ const candidates = [];
36
+ for (const chat of chats) {
37
+ const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
38
+ if (!chat.tcToken?.length || ts <= 0) continue;
39
+ const jid = jidNormalizedUser(chat.id);
40
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
41
+ candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined });
42
+ }
43
+ if (!candidates.length) return;
44
+
45
+ const existing = await keyStore.get('tctoken', candidates.map(c => c.storageJid));
46
+ const entries = {};
47
+ for (const c of candidates) {
48
+ const existingTs = existing[c.storageJid]?.timestamp ? Number(existing[c.storageJid].timestamp) : 0;
49
+ if (existingTs > 0 && existingTs >= c.ts) continue;
50
+ entries[c.storageJid] = { ...existing[c.storageJid], token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) };
51
+ }
52
+ if (!Object.keys(entries).length) return;
53
+
54
+ logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
55
+ try {
56
+ // Include updated __index so cross-session pruning picks these JIDs up.
57
+ const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
58
+ await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
59
+ } catch (err) {
60
+ logger?.warn({ err }, 'failed to store tctokens from history sync');
61
+ }
62
+ }
63
+
64
+ /** Cleans a received message for further processing — strips device/agent from JIDs and corrects key perspective for reactions/polls. */
18
65
  export const cleanMessage = (message, meId, meLid) => {
19
- // ensure remoteJid and participant doesn't have device or agent in it
20
66
  if (isHostedPnUser(message.key.remoteJid) || isHostedLidUser(message.key.remoteJid)) {
21
67
  message.key.remoteJid = jidEncode(jidDecode(message.key?.remoteJid)?.user, isHostedPnUser(message.key.remoteJid) ? 's.whatsapp.net' : 'lid');
22
- }
23
- else {
68
+ } else {
24
69
  message.key.remoteJid = jidNormalizedUser(message.key.remoteJid);
25
70
  }
26
71
  if (isHostedPnUser(message.key.participant) || isHostedLidUser(message.key.participant)) {
27
72
  message.key.participant = jidEncode(jidDecode(message.key.participant)?.user, isHostedPnUser(message.key.participant) ? 's.whatsapp.net' : 'lid');
28
- }
29
- else {
73
+ } else {
30
74
  message.key.participant = jidNormalizedUser(message.key.participant);
31
75
  }
76
+
32
77
  const content = normalizeMessageContent(message.message);
33
- // if the message has a reaction, ensure fromMe & remoteJid are from our perspective
34
- if (content?.reactionMessage) {
35
- normaliseKey(content.reactionMessage.key);
36
- }
37
- if (content?.pollUpdateMessage) {
38
- normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
39
- }
78
+ if (content?.reactionMessage) normaliseKey(content.reactionMessage.key);
79
+ if (content?.pollUpdateMessage) normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
80
+
40
81
  function normaliseKey(msgKey) {
41
- // if the reaction is from another user
42
- // we've to correctly map the key to this user's perspective
43
- if (!message.key.fromMe) {
44
- // if the sender believed the message being reacted to is not from them
45
- // we've to correct the key to be from them, or some other participant
46
- msgKey.fromMe = !msgKey.fromMe
47
- ? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) ||
48
- areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
49
- : // if the message being reacted to, was from them
50
- // fromMe automatically becomes false
51
- false;
52
- // set the remoteJid to being the same as the chat the message came from
53
- // TODO: investigate inconsistencies
54
- msgKey.remoteJid = message.key.remoteJid;
55
- // set participant of the message
56
- msgKey.participant = msgKey.participant || message.key.participant;
57
- }
82
+ if (message.key.fromMe) return;
83
+ msgKey.fromMe = !msgKey.fromMe
84
+ ? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) || areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
85
+ : false; // message was from them fromMe is definitively false from our perspective
86
+ msgKey.remoteJid = message.key.remoteJid; // TODO: investigate inconsistencies
87
+ msgKey.participant = msgKey.participant || message.key.participant;
58
88
  }
59
89
  };
90
+
60
91
  // TODO: target:audit AUDIT THIS FUNCTION AGAIN
61
92
  export const isRealMessage = (message) => {
62
93
  const normalizedContent = normalizeMessageContent(message.message);
63
94
  const hasSomeContent = !!getContentType(normalizedContent);
64
- return ((!!normalizedContent ||
65
- REAL_MSG_STUB_TYPES.has(message.messageStubType) ||
66
- REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
95
+ return (
96
+ (!!normalizedContent || REAL_MSG_STUB_TYPES.has(message.messageStubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
67
97
  hasSomeContent &&
68
98
  !normalizedContent?.protocolMessage &&
69
99
  !normalizedContent?.reactionMessage &&
70
- !normalizedContent?.pollUpdateMessage);
100
+ !normalizedContent?.pollUpdateMessage
101
+ );
71
102
  };
103
+
72
104
  export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
105
+
73
106
  /**
74
- * Get the ID of the chat from the given key.
75
- * Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
107
+ * Derive the chat ID from a message key.
108
+ * For non-status broadcasts the chat is the participant (DM from that sender),
109
+ * for everything else it's the remoteJid.
76
110
  */
77
111
  export const getChatId = ({ remoteJid, participant, fromMe }) => {
78
- if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } })
79
- if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) return participant
80
- return remoteJid
112
+ if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } });
113
+ if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
114
+ if (!participant) throw new Boom('Cannot derive chat id: broadcast message key is missing participant', { data: { remoteJid, fromMe } });
115
+ return participant;
116
+ }
117
+ return remoteJid;
81
118
  };
119
+
82
120
  /**
83
- * Decrypt a poll vote
84
- * @param vote encrypted vote
85
- * @param ctx additional info about the poll required for decryption
86
- * @returns list of SHA256 options
121
+ * Decrypt a poll vote.
122
+ * @returns Decoded PollVoteMessage containing the SHA256 option hashes the voter selected.
87
123
  */
88
124
  export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsgId, pollEncKey, voterJid }) {
89
- const sign = Buffer.concat([
90
- toBinary(pollMsgId),
91
- toBinary(pollCreatorJid),
92
- toBinary(voterJid),
93
- toBinary('Poll Vote'),
94
- new Uint8Array([1])
95
- ]);
125
+ const toBinary = (txt) => Buffer.from(txt);
126
+ const sign = Buffer.concat([toBinary(pollMsgId), toBinary(pollCreatorJid), toBinary(voterJid), toBinary('Poll Vote'), new Uint8Array([1])]);
96
127
  const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256');
97
128
  const decKey = hmacSign(sign, key0, 'sha256');
98
129
  const aad = toBinary(`${pollMsgId}\u0000${voterJid}`);
99
- const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad);
100
- return proto.Message.PollVoteMessage.decode(decrypted);
101
- function toBinary(txt) {
102
- return Buffer.from(txt);
103
- }
130
+ return proto.Message.PollVoteMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
104
131
  }
105
132
 
106
- async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
107
- const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
108
- const candidates = []
109
- for (const chat of chats) {
110
- const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0
111
- if (chat.tcToken?.length && ts > 0) { const jid = jidNormalizedUser(chat.id); const storageJid = await resolveTcTokenJid(jid, getLIDForPN); candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined }) }
112
- }
113
- if (!candidates.length) return
114
- const jids = candidates.map(c => c.storageJid)
115
- const existing = await keyStore.get('tctoken', jids)
116
- const entries = {}
117
- for (const c of candidates) {
118
- const existingEntry = existing[c.storageJid]
119
- const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
120
- if (existingTs > 0 && existingTs >= c.ts) continue
121
- entries[c.storageJid] = { ...existingEntry, token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) }
122
- }
123
- if (Object.keys(entries).length) {
124
- logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync')
125
- try { const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries)); await keyStore.set({ tctoken: { ...entries, ...indexWrite } }) }
126
- catch (err) { logger?.warn({ err }, 'failed to store tctokens from history sync') }
127
- }
133
+ /**
134
+ * Decrypt an event RSVP response.
135
+ * Mirrors decryptPollVote but for EventResponseMessage — uses 'Event Response' as the domain separator.
136
+ * @returns Decoded EventResponseMessage with the responder's RSVP choice.
137
+ */
138
+ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, eventMsgId, eventEncKey, responderJid }) {
139
+ const toBinary = (txt) => Buffer.from(txt);
140
+ const sign = Buffer.concat([toBinary(eventMsgId), toBinary(eventCreatorJid), toBinary(responderJid), toBinary('Event Response'), new Uint8Array([1])]);
141
+ const key0 = hmacSign(eventEncKey, new Uint8Array(32), 'sha256');
142
+ const decKey = hmacSign(sign, key0, 'sha256');
143
+ const aad = toBinary(`${eventMsgId}\u0000${responderJid}`);
144
+ return proto.Message.EventResponseMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
128
145
  }
129
146
 
130
- const SELF_ONLY_TYPES = new Set([proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION, proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE, proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC, proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE])
131
147
 
132
148
  const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => {
133
149
  const meId = creds.me.id;
134
150
  const { accountSettings } = creds;
135
151
  const chat = { id: jidNormalizedUser(getChatId(message.key)) };
136
152
  const isRealMsg = isRealMessage(message);
153
+
137
154
  if (isRealMsg) {
138
155
  chat.messages = [{ message }];
139
156
  chat.conversationTimestamp = toNumber(message.messageTimestamp);
140
- // only increment unread count if not CIPHERTEXT and from another person
141
- if (shouldIncrementChatUnread(message)) {
142
- chat.unreadCount = (chat.unreadCount || 0) + 1;
143
- }
157
+ if (shouldIncrementChatUnread(message)) chat.unreadCount = (chat.unreadCount || 0) + 1;
144
158
  }
159
+
145
160
  const content = normalizeMessageContent(message.message);
146
- // unarchive chat if it's a real message, or someone reacted to our message
147
- // and we've the unarchive chats setting on
161
+
162
+ // Unarchive if real message or someone reacted to our message and the setting is on
148
163
  if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
149
164
  chat.archived = false;
150
165
  chat.readOnly = false;
151
166
  }
167
+
152
168
  const protocolMsg = content?.protocolMessage;
153
169
  if (protocolMsg) {
154
- if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_TYPES.has(protocolMsg.type) && !message.key.fromMe) { logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin'); return }
170
+ // Drop self-only protocol messages that didn't come from our own device
171
+ // an attacker could otherwise spoof history syncs, key shares, etc. to manipulate local state.
172
+ if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_PROTOCOL_TYPES.has(protocolMsg.type) && !message.key.fromMe) {
173
+ logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin');
174
+ return;
175
+ }
176
+
155
177
  switch (protocolMsg.type) {
156
- case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
178
+ case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION: {
157
179
  const histNotification = protocolMsg.historySyncNotification;
158
- const process = shouldProcessHistoryMsg;
159
180
  const isLatest = !creds.processedHistoryMessages?.length;
160
- logger?.info({
161
- histNotification,
162
- process,
163
- id: message.key.id,
164
- isLatest
165
- }, 'got history notification');
166
- if (process) {
181
+ logger?.info({ histNotification, process: shouldProcessHistoryMsg, id: message.key.id, isLatest }, 'got history notification');
182
+ if (shouldProcessHistoryMsg) {
167
183
  if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
168
184
  ev.emit('creds.update', { processedHistoryMessages: [...(creds.processedHistoryMessages || []), { key: message.key, messageTimestamp: message.messageTimestamp }] });
169
185
  }
170
- const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
171
- if (data.lidPnMappings?.length) { logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync'); await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync')) }
172
- await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
186
+ const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger);
187
+ if (data.lidPnMappings?.length) {
188
+ logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync');
189
+ await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
190
+ }
191
+ await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
173
192
  ev.emit('messaging-history.set', { ...data, isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined, chunkOrder: histNotification.chunkOrder, peerDataRequestSessionId: histNotification.peerDataRequestSessionId });
174
193
  }
175
194
  break;
176
- case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
195
+ }
196
+
197
+ case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE: {
177
198
  const keys = protocolMsg.appStateSyncKeyShare.keys;
178
199
  if (keys?.length) {
179
200
  let newAppStateSyncKeyId = '';
@@ -188,68 +209,75 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
188
209
  logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys');
189
210
  }, meId);
190
211
  ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
191
- }
192
- else {
212
+ } else {
193
213
  logger?.info({ protocolMsg }, 'recv app state sync with 0 keys');
194
214
  }
195
215
  break;
196
- case proto.Message.ProtocolMessage.Type.REVOKE:
197
- ev.emit('messages.update', [
198
- {
199
- key: {
200
- ...message.key,
201
- id: protocolMsg.key.id
202
- },
203
- update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
204
- }
205
- ]);
216
+ }
217
+
218
+ case proto.Message.ProtocolMessage.Type.REVOKE: {
219
+ ev.emit('messages.update', [{ key: { ...message.key, id: protocolMsg.key.id }, update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } }]);
206
220
  break;
207
- case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
208
- Object.assign(chat, {
209
- ephemeralSettingTimestamp: toNumber(message.messageTimestamp),
210
- ephemeralExpiration: protocolMsg.ephemeralExpiration || null
211
- });
221
+ }
222
+
223
+ case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING: {
224
+ Object.assign(chat, { ephemeralSettingTimestamp: toNumber(message.messageTimestamp), ephemeralExpiration: protocolMsg.ephemeralExpiration || null });
212
225
  break;
213
- case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
214
- const response = protocolMsg.peerDataOperationRequestResponseMessage;
215
- if (response) {
216
- const peerDataOperationResult = response.peerDataOperationResult || []
217
- for (const result of peerDataOperationResult) {
218
- const retryResponse = result?.placeholderMessageResendResponse
219
- if (!retryResponse?.webMessageInfoBytes) continue
220
- try {
221
- const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
222
- const msgId = webMessageInfo.key?.id
223
- const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined
224
- if (msgId) await placeholderResendCache?.del(msgId)
225
- let finalMsg
226
- if (cachedData && typeof cachedData === 'object') { cachedData.message = webMessageInfo.message; if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp; finalMsg = cachedData }
227
- else finalMsg = webMessageInfo
228
- logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend')
229
- ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId })
230
- } catch (err) { logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response') }
231
- }
226
+ }
227
+
228
+ case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT: {
229
+ ev.emit('messages.update', [{
230
+ // Key is in sender's perspective — flip fromMe so it's in ours
231
+ key: { ...message.key, id: protocolMsg.key?.id },
232
+ update: {
233
+ message: { editedMessage: { message: protocolMsg.editedMessage } },
234
+ messageTimestamp: protocolMsg.timestampMs ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000) : message.messageTimestamp,
235
+ },
236
+ }]);
237
+ break;
238
+ }
239
+
240
+ case proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE: {
241
+ const labelAssociationMsg = protocolMsg.memberLabel;
242
+ if (labelAssociationMsg?.label) {
243
+ ev.emit('group.member-tag.update', { groupId: chat.id, label: labelAssociationMsg.label, participant: message.key.participant, participantAlt: message.key.participantAlt, messageTimestamp: Number(message.messageTimestamp) });
232
244
  }
233
245
  break;
234
- case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
235
- ev.emit('messages.update', [
236
- {
237
- // flip the sender / fromMe properties because they're in the perspective of the sender
238
- key: { ...message.key, id: protocolMsg.key?.id },
239
- update: {
240
- message: {
241
- editedMessage: {
242
- message: protocolMsg.editedMessage
243
- }
244
- },
245
- messageTimestamp: protocolMsg.timestampMs
246
- ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000)
247
- : message.messageTimestamp
246
+ }
247
+
248
+ case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: {
249
+ const response = protocolMsg.peerDataOperationRequestResponseMessage;
250
+ if (!response) break;
251
+ // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.)
252
+ for (const result of (response.peerDataOperationResult || [])) {
253
+ const retryResponse = result?.placeholderMessageResendResponse;
254
+ if (!retryResponse?.webMessageInfoBytes) continue;
255
+ try {
256
+ const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
257
+ const msgId = webMessageInfo.key?.id;
258
+ // Retrieve cached original message data — preserves LID details,
259
+ // timestamps, etc. that the phone may omit in its PDO response
260
+ const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined;
261
+ if (msgId) await placeholderResendCache?.del(msgId);
262
+ let finalMsg;
263
+ if (cachedData && typeof cachedData === 'object') {
264
+ // Apply decoded message content onto cached metadata (preserves LID etc.)
265
+ cachedData.message = webMessageInfo.message;
266
+ if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
267
+ finalMsg = cachedData;
268
+ } else {
269
+ finalMsg = webMessageInfo;
248
270
  }
271
+ logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend');
272
+ ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId });
273
+ } catch (err) {
274
+ logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response');
249
275
  }
250
- ]);
276
+ }
251
277
  break;
252
- case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
278
+ }
279
+
280
+ case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC: {
253
281
  const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload;
254
282
  const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload);
255
283
  logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp');
@@ -259,82 +287,73 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
259
287
  pairs.push({ lid: `${lid}@lid`, pn: `${pn}@s.whatsapp.net` });
260
288
  }
261
289
  await signalRepository.lidMapping.storeLIDPNMappings(pairs);
262
- if (pairs.length) {
263
- for (const { pn, lid } of pairs) {
264
- await signalRepository.migrateSession(pn, lid);
265
- }
266
- }
290
+ for (const { pn, lid } of pairs) await signalRepository.migrateSession(pn, lid);
291
+ break;
292
+ }
267
293
  }
268
- }
269
- else if (content?.reactionMessage) {
270
- const reaction = {
271
- ...content.reactionMessage,
272
- key: message.key
273
- };
274
- ev.emit('messages.reaction', [
275
- {
276
- reaction,
277
- key: content.reactionMessage?.key
294
+ } else if (content?.reactionMessage) {
295
+ ev.emit('messages.reaction', [{ reaction: { ...content.reactionMessage, key: message.key }, key: content.reactionMessage?.key }]);
296
+ } else if (content?.encEventResponseMessage) {
297
+ // Decrypt and re-emit event RSVPs — mirrors the poll vote flow but for calendar events
298
+ const encEventResponse = content.encEventResponseMessage;
299
+ const creationMsgKey = encEventResponse.eventCreationMessageKey;
300
+ const eventMsg = await getMessage(creationMsgKey);
301
+ if (eventMsg) {
302
+ try {
303
+ const meIdNormalised = jidNormalizedUser(meId);
304
+ // Event creator JID must be a PN — resolve from LID if necessary
305
+ const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid;
306
+ const eventCreatorPn = isLidUser(eventCreatorKey) ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) : eventCreatorKey;
307
+ const eventCreatorJid = getKeyAuthor({ remoteJid: jidNormalizedUser(eventCreatorPn), fromMe: meIdNormalised === eventCreatorPn }, meIdNormalised);
308
+ const responderJid = getKeyAuthor(message.key, meIdNormalised);
309
+ const eventEncKey = eventMsg?.messageContextInfo?.messageSecret;
310
+ if (!eventEncKey) {
311
+ logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption');
312
+ } else {
313
+ const responseMsg = decryptEventResponse(encEventResponse, { eventEncKey, eventCreatorJid, eventMsgId: creationMsgKey.id, responderJid });
314
+ ev.emit('messages.update', [{ key: creationMsgKey, update: { eventResponses: [{ eventResponseMessageKey: message.key, senderTimestampMs: responseMsg.timestampMs, response: responseMsg }] } }]);
315
+ }
316
+ } catch (err) {
317
+ logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response');
278
318
  }
279
- ]);
280
- }
281
- else if (message.messageStubType) {
319
+ } else {
320
+ logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response');
321
+ }
322
+ } else if (message.messageStubType) {
282
323
  const jid = message.key?.remoteJid;
283
- //let actor = whatsappID (message.participant)
284
324
  let participants;
285
- const emitParticipantsUpdate = (action) => ev.emit('group-participants.update', {
286
- id: jid,
287
- author: message.key.participant,
288
- authorPn: message.key.participantAlt,
289
- participants,
290
- action
291
- });
292
- const emitGroupUpdate = (update) => {
293
- ev.emit('groups.update', [
294
- { id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
295
- ]);
296
- };
297
- const emitGroupRequestJoin = (participant, action, method) => {
298
- ev.emit('group.join-request', {
299
- id: jid,
300
- author: message.key.participant,
301
- authorPn: message.key.participantAlt,
302
- participant: participant.lid,
303
- participantPn: participant.pn,
304
- action,
305
- method: method
306
- });
307
- };
308
- const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)); // ADD SUPPORT FOR LID
325
+
326
+ const emitParticipantsUpdate = (action) => ev.emit('group-participants.update', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participants, action });
327
+ const emitGroupUpdate = (update) => ev.emit('groups.update', [{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }]);
328
+ const emitGroupRequestJoin = (participant, action, method) => ev.emit('group.join-request', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participant: participant.lid, participantPn: participant.pn, action, method });
329
+
330
+ // TODO: ADD SUPPORT FOR LID in participantsIncludesMe
331
+ const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.phoneNumber));
332
+
309
333
  switch (message.messageStubType) {
310
334
  case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
311
- participants = message.messageStubParameters.map(a => { try { return JSON.parse(a) } catch { return { id: a } } }) || []
312
- emitParticipantsUpdate('modify')
313
- break
335
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
336
+ emitParticipantsUpdate('modify');
337
+ break;
314
338
  case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
315
339
  case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
316
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
340
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
317
341
  emitParticipantsUpdate('remove');
318
- // mark the chat read only if you left the group
319
- if (participantsIncludesMe()) {
320
- chat.readOnly = true;
321
- }
342
+ if (participantsIncludesMe()) chat.readOnly = true;
322
343
  break;
323
344
  case WAMessageStubType.GROUP_PARTICIPANT_ADD:
324
345
  case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
325
346
  case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
326
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
327
- if (participantsIncludesMe()) {
328
- chat.readOnly = false;
329
- }
347
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
348
+ if (participantsIncludesMe()) chat.readOnly = false;
330
349
  emitParticipantsUpdate('add');
331
350
  break;
332
351
  case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
333
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
352
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
334
353
  emitParticipantsUpdate('demote');
335
354
  break;
336
355
  case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
337
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
356
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
338
357
  emitParticipantsUpdate('promote');
339
358
  break;
340
359
  case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
@@ -423,9 +442,9 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
423
442
  )
424
443
  }
425
444
  } */
426
- if (Object.keys(chat).length > 1) {
427
- ev.emit('chats.update', [chat]);
428
- }
445
+
446
+ if (Object.keys(chat).length > 1) ev.emit('chats.update', [chat]);
429
447
  };
448
+
430
449
  export default processMessage;
431
450
  //# sourceMappingURL=process-message.js.map