@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +6 -0
- package/WAProto/fix-imports.js +70 -18
- package/WAProto/index.js +197 -160
- package/lib/Defaults/index.js +17 -4
- package/lib/Signal/libsignal.js +63 -2
- package/lib/Signal/lid-mapping.js +170 -70
- package/lib/Socket/Client/websocket.js +5 -1
- package/lib/Socket/business.js +11 -8
- package/lib/Socket/chats.js +55 -28
- package/lib/Socket/index.js +0 -6
- package/lib/Socket/messages-recv.js +152 -75
- package/lib/Socket/messages-send.js +230 -148
- package/lib/Socket/socket.js +69 -15
- package/lib/Utils/auth-utils.js +53 -20
- package/lib/Utils/chat-utils.js +100 -51
- package/lib/Utils/crypto.js +2 -26
- package/lib/Utils/event-buffer.js +33 -7
- package/lib/Utils/generics.js +4 -1
- package/lib/Utils/history.js +46 -5
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/lt-hash.js +2 -42
- package/lib/Utils/make-mutex.js +20 -27
- package/lib/Utils/message-retry-manager.js +58 -5
- package/lib/Utils/messages-media.js +151 -40
- package/lib/Utils/messages.js +43 -23
- package/lib/Utils/noise-handler.js +139 -85
- package/lib/Utils/process-message.js +57 -14
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tc-token-utils.js +18 -0
- package/lib/Utils/use-sqlite-auth-state.js +122 -0
- package/lib/WABinary/decode.js +24 -0
- package/lib/WABinary/encode.js +5 -1
- package/lib/WABinary/generic-utils.js +19 -8
- package/package.json +7 -2
|
@@ -28,6 +28,7 @@ export const makeEventBuffer = (logger) => {
|
|
|
28
28
|
let data = makeBufferData();
|
|
29
29
|
let isBuffering = false;
|
|
30
30
|
let bufferTimeout = null;
|
|
31
|
+
let flushPendingTimeout = null; // Add a specific timer for the debounced flush to prevent leak
|
|
31
32
|
let bufferCount = 0;
|
|
32
33
|
const MAX_HISTORY_CACHE_SIZE = 10000; // Limit the history cache size to prevent memory bloat
|
|
33
34
|
const BUFFER_TIMEOUT_MS = 30000; // 30 seconds
|
|
@@ -41,8 +42,7 @@ export const makeEventBuffer = (logger) => {
|
|
|
41
42
|
if (!isBuffering) {
|
|
42
43
|
logger.debug('Event buffer activated');
|
|
43
44
|
isBuffering = true;
|
|
44
|
-
bufferCount
|
|
45
|
-
// Auto-flush after a timeout to prevent infinite buffering
|
|
45
|
+
bufferCount = 0;
|
|
46
46
|
if (bufferTimeout) {
|
|
47
47
|
clearTimeout(bufferTimeout);
|
|
48
48
|
}
|
|
@@ -53,9 +53,8 @@ export const makeEventBuffer = (logger) => {
|
|
|
53
53
|
}
|
|
54
54
|
}, BUFFER_TIMEOUT_MS);
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
56
|
+
// Always increment count when requested
|
|
57
|
+
bufferCount++;
|
|
59
58
|
}
|
|
60
59
|
function flush() {
|
|
61
60
|
if (!isBuffering) {
|
|
@@ -69,6 +68,10 @@ export const makeEventBuffer = (logger) => {
|
|
|
69
68
|
clearTimeout(bufferTimeout);
|
|
70
69
|
bufferTimeout = null;
|
|
71
70
|
}
|
|
71
|
+
if (flushPendingTimeout) {
|
|
72
|
+
clearTimeout(flushPendingTimeout);
|
|
73
|
+
flushPendingTimeout = null;
|
|
74
|
+
}
|
|
72
75
|
// Clear history cache if it exceeds the max size
|
|
73
76
|
if (historyCache.size > MAX_HISTORY_CACHE_SIZE) {
|
|
74
77
|
logger.debug({ cacheSize: historyCache.size }, 'Clearing history cache');
|
|
@@ -103,6 +106,27 @@ export const makeEventBuffer = (logger) => {
|
|
|
103
106
|
};
|
|
104
107
|
},
|
|
105
108
|
emit(event, evData) {
|
|
109
|
+
// Check if this is a messages.upsert with a different type than what's buffered
|
|
110
|
+
// If so, flush the buffered messages first to avoid type overshadowing
|
|
111
|
+
if (event === 'messages.upsert') {
|
|
112
|
+
const { type } = evData;
|
|
113
|
+
const existingUpserts = Object.values(data.messageUpserts);
|
|
114
|
+
if (existingUpserts.length > 0) {
|
|
115
|
+
const bufferedType = existingUpserts[0].type;
|
|
116
|
+
if (bufferedType !== type) {
|
|
117
|
+
logger.debug({ bufferedType, newType: type }, 'messages.upsert type mismatch, emitting buffered messages');
|
|
118
|
+
// Emit the buffered messages with their correct type
|
|
119
|
+
ev.emit('event', {
|
|
120
|
+
'messages.upsert': {
|
|
121
|
+
messages: existingUpserts.map(m => m.message),
|
|
122
|
+
type: bufferedType
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Clear the message upserts from the buffer
|
|
126
|
+
data.messageUpserts = {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
106
130
|
if (isBuffering && BUFFERABLE_EVENT_SET.has(event)) {
|
|
107
131
|
append(data, historyCache, event, evData, logger);
|
|
108
132
|
return true;
|
|
@@ -135,8 +159,10 @@ export const makeEventBuffer = (logger) => {
|
|
|
135
159
|
finally {
|
|
136
160
|
bufferCount = Math.max(0, bufferCount - 1);
|
|
137
161
|
if (bufferCount === 0) {
|
|
138
|
-
//
|
|
139
|
-
|
|
162
|
+
// Only schedule ONE timeout, not 10,000
|
|
163
|
+
if (!flushPendingTimeout) {
|
|
164
|
+
flushPendingTimeout = setTimeout(flush, 100);
|
|
165
|
+
}
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
};
|
package/lib/Utils/generics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Boom } from '@hapi/boom';
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
|
-
const baileysVersion = [2, 3000,
|
|
4
|
+
const baileysVersion = [2, 3000, 1033105955];
|
|
5
5
|
import { DisconnectReason } from '../Types/index.js';
|
|
6
6
|
import { getAllBinaryNodeChildren, jidDecode } from '../WABinary/index.js';
|
|
7
7
|
import { sha256 } from './crypto.js';
|
|
@@ -31,6 +31,9 @@ export const BufferJSON = {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
export const getKeyAuthor = (key, meId = 'me') => (key?.fromMe ? meId : key?.participantAlt || key?.remoteJidAlt || key?.participant || key?.remoteJid) || '';
|
|
34
|
+
export const isStringNullOrEmpty = (value) =>
|
|
35
|
+
// eslint-disable-next-line eqeqeq
|
|
36
|
+
value == null || value === '';
|
|
34
37
|
export const writeRandomPadMax16 = (msg) => {
|
|
35
38
|
const pad = randomBytes(1);
|
|
36
39
|
const padLength = (pad[0] & 0x0f) + 1;
|
package/lib/Utils/history.js
CHANGED
|
@@ -2,10 +2,26 @@ import { promisify } from 'util';
|
|
|
2
2
|
import { inflate } from 'zlib';
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
4
|
import { WAMessageStubType } from '../Types/index.js';
|
|
5
|
+
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/index.js';
|
|
5
6
|
import { toNumber } from './generics.js';
|
|
6
7
|
import { normalizeMessageContent } from './messages.js';
|
|
7
8
|
import { downloadContentFromMessage } from './messages-media.js';
|
|
8
9
|
const inflatePromise = promisify(inflate);
|
|
10
|
+
const extractPnFromMessages = (messages) => {
|
|
11
|
+
for (const msgItem of messages) {
|
|
12
|
+
const message = msgItem.message;
|
|
13
|
+
// Only extract from outgoing messages (fromMe: true) in 1:1 chats
|
|
14
|
+
// because userReceipt.userJid is the recipient's JID
|
|
15
|
+
if (!message?.key?.fromMe || !message.userReceipt?.length) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const userJid = message.userReceipt[0]?.userJid;
|
|
19
|
+
if (userJid && (isPnUser(userJid) || isHostedPnUser(userJid))) {
|
|
20
|
+
return userJid;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
9
25
|
export const downloadHistory = async (msg, options) => {
|
|
10
26
|
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options });
|
|
11
27
|
const bufferArray = [];
|
|
@@ -18,10 +34,18 @@ export const downloadHistory = async (msg, options) => {
|
|
|
18
34
|
const syncData = proto.HistorySync.decode(buffer);
|
|
19
35
|
return syncData;
|
|
20
36
|
};
|
|
21
|
-
export const processHistoryMessage = (item) => {
|
|
37
|
+
export const processHistoryMessage = (item, logger) => {
|
|
22
38
|
const messages = [];
|
|
23
39
|
const contacts = [];
|
|
24
40
|
const chats = [];
|
|
41
|
+
const lidPnMappings = [];
|
|
42
|
+
logger?.trace({ progress: item.progress }, 'processing history of type ' + item.syncType?.toString());
|
|
43
|
+
// Extract LID-PN mappings for all sync types
|
|
44
|
+
for (const m of item.phoneNumberToLidMappings || []) {
|
|
45
|
+
if (m.lidJid && m.pnJid) {
|
|
46
|
+
lidPnMappings.push({ lid: m.lidJid, pn: m.pnJid });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
25
49
|
switch (item.syncType) {
|
|
26
50
|
case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
|
|
27
51
|
case proto.HistorySync.HistorySyncType.RECENT:
|
|
@@ -30,10 +54,26 @@ export const processHistoryMessage = (item) => {
|
|
|
30
54
|
for (const chat of item.conversations) {
|
|
31
55
|
contacts.push({
|
|
32
56
|
id: chat.id,
|
|
33
|
-
name: chat.name || undefined,
|
|
34
|
-
lid: chat.lidJid || undefined,
|
|
57
|
+
name: chat.displayName || chat.name || chat.username || undefined,
|
|
58
|
+
lid: chat.lidJid || chat.accountLid || undefined,
|
|
35
59
|
phoneNumber: chat.pnJid || undefined
|
|
36
60
|
});
|
|
61
|
+
const chatId = chat.id;
|
|
62
|
+
const isLid = isLidUser(chatId) || isHostedLidUser(chatId);
|
|
63
|
+
const isPn = isPnUser(chatId) || isHostedPnUser(chatId);
|
|
64
|
+
if (isLid && chat.pnJid) {
|
|
65
|
+
lidPnMappings.push({ lid: chatId, pn: chat.pnJid });
|
|
66
|
+
}
|
|
67
|
+
else if (isPn && chat.lidJid) {
|
|
68
|
+
lidPnMappings.push({ lid: chat.lidJid, pn: chatId });
|
|
69
|
+
}
|
|
70
|
+
else if (isLid && !chat.pnJid) {
|
|
71
|
+
// Fallback: extract PN from userReceipt in messages when pnJid is missing
|
|
72
|
+
const pnFromReceipt = extractPnFromMessages(chat.messages || []);
|
|
73
|
+
if (pnFromReceipt) {
|
|
74
|
+
lidPnMappings.push({ lid: chatId, pn: pnFromReceipt });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
37
77
|
const msgs = chat.messages || [];
|
|
38
78
|
delete chat.messages;
|
|
39
79
|
for (const item of msgs) {
|
|
@@ -68,11 +108,12 @@ export const processHistoryMessage = (item) => {
|
|
|
68
108
|
chats,
|
|
69
109
|
contacts,
|
|
70
110
|
messages,
|
|
111
|
+
lidPnMappings,
|
|
71
112
|
syncType: item.syncType,
|
|
72
113
|
progress: item.progress
|
|
73
114
|
};
|
|
74
115
|
};
|
|
75
|
-
export const downloadAndProcessHistorySyncNotification = async (msg, options) => {
|
|
116
|
+
export const downloadAndProcessHistorySyncNotification = async (msg, options, logger) => {
|
|
76
117
|
let historyMsg;
|
|
77
118
|
if (msg.initialHistBootstrapInlinePayload) {
|
|
78
119
|
historyMsg = proto.HistorySync.decode(await inflatePromise(msg.initialHistBootstrapInlinePayload));
|
|
@@ -80,7 +121,7 @@ export const downloadAndProcessHistorySyncNotification = async (msg, options) =>
|
|
|
80
121
|
else {
|
|
81
122
|
historyMsg = await downloadHistory(msg, options);
|
|
82
123
|
}
|
|
83
|
-
return processHistoryMessage(historyMsg);
|
|
124
|
+
return processHistoryMessage(historyMsg, logger);
|
|
84
125
|
};
|
|
85
126
|
export const getHistoryMsg = (message) => {
|
|
86
127
|
const normalizedContent = !!message ? normalizeMessageContent(message) : undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import NodeCache from '@cacheable/node-cache';
|
|
2
|
+
import { areJidsSameUser, getBinaryNodeChild, jidDecode } from '../WABinary/index.js';
|
|
3
|
+
import { isStringNullOrEmpty } from './generics.js';
|
|
4
|
+
export async function handleIdentityChange(node, ctx) {
|
|
5
|
+
const from = node.attrs.from;
|
|
6
|
+
if (!from) {
|
|
7
|
+
return { action: 'invalid_notification' };
|
|
8
|
+
}
|
|
9
|
+
const identityNode = getBinaryNodeChild(node, 'identity');
|
|
10
|
+
if (!identityNode) {
|
|
11
|
+
return { action: 'no_identity_node' };
|
|
12
|
+
}
|
|
13
|
+
ctx.logger.info({ jid: from }, 'identity changed');
|
|
14
|
+
const decoded = jidDecode(from);
|
|
15
|
+
if (decoded?.device && decoded.device !== 0) {
|
|
16
|
+
ctx.logger.debug({ jid: from, device: decoded.device }, 'ignoring identity change from companion device');
|
|
17
|
+
return { action: 'skipped_companion_device', device: decoded.device };
|
|
18
|
+
}
|
|
19
|
+
const isSelfPrimary = ctx.meId && (areJidsSameUser(from, ctx.meId) || (ctx.meLid && areJidsSameUser(from, ctx.meLid)));
|
|
20
|
+
if (isSelfPrimary) {
|
|
21
|
+
ctx.logger.info({ jid: from }, 'self primary identity changed');
|
|
22
|
+
return { action: 'skipped_self_primary' };
|
|
23
|
+
}
|
|
24
|
+
if (ctx.debounceCache.get(from)) {
|
|
25
|
+
ctx.logger.debug({ jid: from }, 'skipping identity assert (debounced)');
|
|
26
|
+
return { action: 'debounced' };
|
|
27
|
+
}
|
|
28
|
+
ctx.debounceCache.set(from, true);
|
|
29
|
+
const isOfflineNotification = !isStringNullOrEmpty(node.attrs.offline);
|
|
30
|
+
const hasExistingSession = await ctx.validateSession(from);
|
|
31
|
+
if (!hasExistingSession.exists) {
|
|
32
|
+
ctx.logger.debug({ jid: from }, 'no old session, skipping session refresh');
|
|
33
|
+
return { action: 'skipped_no_session' };
|
|
34
|
+
}
|
|
35
|
+
ctx.logger.debug({ jid: from }, 'old session exists, will refresh session');
|
|
36
|
+
if (isOfflineNotification) {
|
|
37
|
+
ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing');
|
|
38
|
+
return { action: 'skipped_offline' };
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await ctx.assertSessions([from], true);
|
|
42
|
+
return { action: 'session_refreshed' };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
ctx.logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
|
|
46
|
+
return { action: 'session_refresh_failed', error };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=identity-change-handler.js.map
|
package/lib/Utils/index.js
CHANGED
|
@@ -16,4 +16,6 @@ export * from './event-buffer.js';
|
|
|
16
16
|
export * from './process-message.js';
|
|
17
17
|
export * from './message-retry-manager.js';
|
|
18
18
|
export * from './browser-utils.js';
|
|
19
|
+
export * from './identity-change-handler.js';
|
|
20
|
+
export * from './use-sqlite-auth-state.js';
|
|
19
21
|
//# sourceMappingURL=index.js.map
|
package/lib/Utils/lt-hash.js
CHANGED
|
@@ -1,48 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LTHashAntiTampering } from 'whatsapp-rust-bridge';
|
|
2
2
|
/**
|
|
3
3
|
* LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data
|
|
4
4
|
* over a series of mutations. You can add/remove mutations and it'll return a hash equal to
|
|
5
5
|
* if the same series of mutations was made sequentially.
|
|
6
6
|
*/
|
|
7
|
-
const
|
|
8
|
-
class LTHash {
|
|
9
|
-
constructor(e) {
|
|
10
|
-
this.salt = e;
|
|
11
|
-
}
|
|
12
|
-
async add(e, t) {
|
|
13
|
-
for (const item of t) {
|
|
14
|
-
e = await this._addSingle(e, item);
|
|
15
|
-
}
|
|
16
|
-
return e;
|
|
17
|
-
}
|
|
18
|
-
async subtract(e, t) {
|
|
19
|
-
for (const item of t) {
|
|
20
|
-
e = await this._subtractSingle(e, item);
|
|
21
|
-
}
|
|
22
|
-
return e;
|
|
23
|
-
}
|
|
24
|
-
async subtractThenAdd(e, addList, subtractList) {
|
|
25
|
-
const subtracted = await this.subtract(e, subtractList);
|
|
26
|
-
return this.add(subtracted, addList);
|
|
27
|
-
}
|
|
28
|
-
async _addSingle(e, t) {
|
|
29
|
-
const derived = new Uint8Array(await hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
|
|
30
|
-
return this.performPointwiseWithOverflow(e, derived, (a, b) => a + b);
|
|
31
|
-
}
|
|
32
|
-
async _subtractSingle(e, t) {
|
|
33
|
-
const derived = new Uint8Array(await hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
|
|
34
|
-
return this.performPointwiseWithOverflow(e, derived, (a, b) => a - b);
|
|
35
|
-
}
|
|
36
|
-
performPointwiseWithOverflow(e, t, op) {
|
|
37
|
-
const n = new DataView(e);
|
|
38
|
-
const i = new DataView(t);
|
|
39
|
-
const out = new ArrayBuffer(n.byteLength);
|
|
40
|
-
const s = new DataView(out);
|
|
41
|
-
for (let offset = 0; offset < n.byteLength; offset += 2) {
|
|
42
|
-
s.setUint16(offset, op(n.getUint16(offset, true), i.getUint16(offset, true)), true);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
export const LT_HASH_ANTI_TAMPERING = new LTHash('WhatsApp Patch Integrity');
|
|
7
|
+
export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering();
|
|
48
8
|
//# sourceMappingURL=lt-hash.js.map
|
package/lib/Utils/make-mutex.js
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
|
+
import { Mutex as AsyncMutex } from 'async-mutex';
|
|
1
2
|
export const makeMutex = () => {
|
|
2
|
-
|
|
3
|
-
let task = Promise.resolve();
|
|
4
|
-
let taskTimeout;
|
|
3
|
+
const mutex = new AsyncMutex();
|
|
5
4
|
return {
|
|
6
5
|
mutex(code) {
|
|
7
|
-
|
|
8
|
-
// wait for the previous task to complete
|
|
9
|
-
// if there is an error, we swallow so as to not block the queue
|
|
10
|
-
try {
|
|
11
|
-
await task;
|
|
12
|
-
}
|
|
13
|
-
catch { }
|
|
14
|
-
try {
|
|
15
|
-
// execute the current task
|
|
16
|
-
const result = await code();
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
finally {
|
|
20
|
-
clearTimeout(taskTimeout);
|
|
21
|
-
}
|
|
22
|
-
})();
|
|
23
|
-
// we replace the existing task, appending the new piece of execution to it
|
|
24
|
-
// so the next task will have to wait for this one to finish
|
|
25
|
-
return task;
|
|
6
|
+
return mutex.runExclusive(code);
|
|
26
7
|
}
|
|
27
8
|
};
|
|
28
9
|
};
|
|
29
10
|
export const makeKeyedMutex = () => {
|
|
30
|
-
const map =
|
|
11
|
+
const map = new Map();
|
|
31
12
|
return {
|
|
32
|
-
mutex(key, task) {
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
async mutex(key, task) {
|
|
14
|
+
let entry = map.get(key);
|
|
15
|
+
if (!entry) {
|
|
16
|
+
entry = { mutex: new AsyncMutex(), refCount: 0 };
|
|
17
|
+
map.set(key, entry);
|
|
18
|
+
}
|
|
19
|
+
entry.refCount++;
|
|
20
|
+
try {
|
|
21
|
+
return await entry.mutex.runExclusive(task);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
entry.refCount--;
|
|
25
|
+
// only delete it if this is still the current entry
|
|
26
|
+
if (entry.refCount === 0 && map.get(key) === entry) {
|
|
27
|
+
map.delete(key);
|
|
28
|
+
}
|
|
35
29
|
}
|
|
36
|
-
return map[key].mutex(task);
|
|
37
30
|
}
|
|
38
31
|
};
|
|
39
32
|
};
|
|
@@ -5,6 +5,28 @@ const MESSAGE_KEY_SEPARATOR = '\u0000';
|
|
|
5
5
|
/** Timeout for session recreation - 1 hour */
|
|
6
6
|
const RECREATE_SESSION_TIMEOUT = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
7
7
|
const PHONE_REQUEST_DELAY = 3000;
|
|
8
|
+
// Retry reason codes matching WhatsApp Web's Signal error codes.
|
|
9
|
+
export var RetryReason;
|
|
10
|
+
(function (RetryReason) {
|
|
11
|
+
RetryReason[RetryReason["UnknownError"] = 0] = "UnknownError";
|
|
12
|
+
RetryReason[RetryReason["SignalErrorNoSession"] = 1] = "SignalErrorNoSession";
|
|
13
|
+
RetryReason[RetryReason["SignalErrorInvalidKey"] = 2] = "SignalErrorInvalidKey";
|
|
14
|
+
RetryReason[RetryReason["SignalErrorInvalidKeyId"] = 3] = "SignalErrorInvalidKeyId";
|
|
15
|
+
/** MAC verification failed - most common cause of decryption failures */
|
|
16
|
+
RetryReason[RetryReason["SignalErrorInvalidMessage"] = 4] = "SignalErrorInvalidMessage";
|
|
17
|
+
RetryReason[RetryReason["SignalErrorInvalidSignature"] = 5] = "SignalErrorInvalidSignature";
|
|
18
|
+
RetryReason[RetryReason["SignalErrorFutureMessage"] = 6] = "SignalErrorFutureMessage";
|
|
19
|
+
/** Explicit MAC failure - session is definitely out of sync */
|
|
20
|
+
RetryReason[RetryReason["SignalErrorBadMac"] = 7] = "SignalErrorBadMac";
|
|
21
|
+
RetryReason[RetryReason["SignalErrorInvalidSession"] = 8] = "SignalErrorInvalidSession";
|
|
22
|
+
RetryReason[RetryReason["SignalErrorInvalidMsgKey"] = 9] = "SignalErrorInvalidMsgKey";
|
|
23
|
+
RetryReason[RetryReason["BadBroadcastEphemeralSetting"] = 10] = "BadBroadcastEphemeralSetting";
|
|
24
|
+
RetryReason[RetryReason["UnknownCompanionNoPrekey"] = 11] = "UnknownCompanionNoPrekey";
|
|
25
|
+
RetryReason[RetryReason["AdvFailure"] = 12] = "AdvFailure";
|
|
26
|
+
RetryReason[RetryReason["StatusRevokeDelay"] = 13] = "StatusRevokeDelay";
|
|
27
|
+
})(RetryReason || (RetryReason = {}));
|
|
28
|
+
/** Error codes that indicate a MAC failure and require immediate session recreation */
|
|
29
|
+
const MAC_ERROR_CODES = new Set([RetryReason.SignalErrorInvalidMessage, RetryReason.SignalErrorBadMac]);
|
|
8
30
|
export class MessageRetryManager {
|
|
9
31
|
constructor(logger, maxMsgRetryCount) {
|
|
10
32
|
this.logger = logger;
|
|
@@ -65,9 +87,10 @@ export class MessageRetryManager {
|
|
|
65
87
|
return this.recentMessagesMap.get(keyStr);
|
|
66
88
|
}
|
|
67
89
|
/**
|
|
68
|
-
* Check if a session should be recreated based on retry count and
|
|
90
|
+
* Check if a session should be recreated based on retry count, history, and error code.
|
|
91
|
+
* MAC errors (codes 4 and 7) trigger immediate session recreation regardless of timeout.
|
|
69
92
|
*/
|
|
70
|
-
shouldRecreateSession(jid,
|
|
93
|
+
shouldRecreateSession(jid, hasSession, errorCode) {
|
|
71
94
|
// If we don't have a session, always recreate
|
|
72
95
|
if (!hasSession) {
|
|
73
96
|
this.sessionRecreateHistory.set(jid, Date.now());
|
|
@@ -77,9 +100,15 @@ export class MessageRetryManager {
|
|
|
77
100
|
recreate: true
|
|
78
101
|
};
|
|
79
102
|
}
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
82
|
-
|
|
103
|
+
// IMMEDIATE recreation for MAC errors - session is definitely out of sync
|
|
104
|
+
if (errorCode !== undefined && MAC_ERROR_CODES.has(errorCode)) {
|
|
105
|
+
this.sessionRecreateHistory.set(jid, Date.now());
|
|
106
|
+
this.statistics.sessionRecreations++;
|
|
107
|
+
this.logger.warn({ jid, errorCode: RetryReason[errorCode] }, 'MAC error detected, forcing immediate session recreation');
|
|
108
|
+
return {
|
|
109
|
+
reason: `MAC error (code ${errorCode}: ${RetryReason[errorCode]}), immediate session recreation`,
|
|
110
|
+
recreate: true
|
|
111
|
+
};
|
|
83
112
|
}
|
|
84
113
|
const now = Date.now();
|
|
85
114
|
const prevTime = this.sessionRecreateHistory.get(jid);
|
|
@@ -94,6 +123,30 @@ export class MessageRetryManager {
|
|
|
94
123
|
}
|
|
95
124
|
return { reason: '', recreate: false };
|
|
96
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Parse error code from retry receipt's retry node.
|
|
128
|
+
* Returns undefined if no error code is present.
|
|
129
|
+
*/
|
|
130
|
+
parseRetryErrorCode(errorAttr) {
|
|
131
|
+
if (errorAttr === undefined || errorAttr === '') {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const code = parseInt(errorAttr, 10);
|
|
135
|
+
if (Number.isNaN(code)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
// Validate it's a known RetryReason
|
|
139
|
+
if (code >= RetryReason.UnknownError && code <= RetryReason.StatusRevokeDelay) {
|
|
140
|
+
return code;
|
|
141
|
+
}
|
|
142
|
+
return RetryReason.UnknownError;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if an error code indicates a MAC failure
|
|
146
|
+
*/
|
|
147
|
+
isMacError(errorCode) {
|
|
148
|
+
return errorCode !== undefined && MAC_ERROR_CODES.has(errorCode);
|
|
149
|
+
}
|
|
97
150
|
/**
|
|
98
151
|
* Increment retry counter for a message
|
|
99
152
|
*/
|