@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.
@@ -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
- else {
57
- bufferCount++;
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
- // Auto-flush when no other buffers are active
139
- setTimeout(flush, 100);
162
+ // Only schedule ONE timeout, not 10,000
163
+ if (!flushPendingTimeout) {
164
+ flushPendingTimeout = setTimeout(flush, 100);
165
+ }
140
166
  }
141
167
  }
142
168
  };
@@ -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, 1027934701];
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;
@@ -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
@@ -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
@@ -1,48 +1,8 @@
1
- import { hkdf } from './crypto.js';
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 o = 128;
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
@@ -1,39 +1,32 @@
1
+ import { Mutex as AsyncMutex } from 'async-mutex';
1
2
  export const makeMutex = () => {
2
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
- let task = Promise.resolve();
4
- let taskTimeout;
3
+ const mutex = new AsyncMutex();
5
4
  return {
6
5
  mutex(code) {
7
- task = (async () => {
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
- if (!map[key]) {
34
- map[key] = makeMutex();
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 history
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, retryCount, hasSession) {
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
- // Only consider recreation if retry count > 1
81
- if (retryCount < 2) {
82
- return { reason: '', recreate: false };
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
  */