@nexustechpro/baileys 2.1.0 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/Defaults/index.js +1 -0
- package/lib/Socket/chats.js +180 -272
- package/lib/Socket/messages-recv.js +8 -5
- package/lib/Socket/messages-send.js +16 -15
- package/lib/Utils/chat-utils.js +318 -619
- package/lib/Utils/key-store.js +10 -5
- package/lib/Utils/lt-hash.js +2 -43
- package/lib/Utils/messages.js +89 -64
- package/lib/Utils/process-message.js +234 -215
- package/lib/Utils/tc-token-utils.js +38 -73
- package/lib/Utils/use-multi-file-auth-state.js +18 -8
- package/lib/index.js +1 -1
- package/package.json +2 -2
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
if (content?.
|
|
35
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 (
|
|
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
|
-
*
|
|
75
|
-
*
|
|
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)
|
|
80
|
-
|
|
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
|
-
* @
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
}
|
|
290
|
+
for (const { pn, lid } of pairs) await signalRepository.migrateSession(pn, lid);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
267
293
|
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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 =>
|
|
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(
|
|
340
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
317
341
|
emitParticipantsUpdate('remove');
|
|
318
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
427
|
-
|
|
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
|