kage-baileys 2.0.0 → 3.0.0

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/WAProto/p.html CHANGED
@@ -1 +1 @@
1
- wileys
1
+ kage
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  const [major, minor] = process.versions.node.split('.').map(Number);
3
3
  if (major < 22 || (major === 22 && minor < 12)) {
4
- console.error('Kage Baileys requires Node.js >=22.12.0. Use a supported Node.js LTS release.');
4
+ console.error('Kage Baileys necesita Node >=22.12.0 por interoperabilidad CommonJS/ESM. Selecciona Node 22 o 24 en el panel. No basta con borrar engines.');
5
5
  process.exitCode = 1;
6
6
  }
@@ -65,7 +65,7 @@ exports.default = (config = {}) => {
65
65
  return Object.keys(contacts);
66
66
  };
67
67
  /**
68
- * binds to a WileysEventEmitter.
68
+ * binds to a KageEventEmitter.
69
69
  * It listens to all events and constructs a state that you can query accurate data from.
70
70
  * Eg. can use the store to fetch chats, contacts, messages etc.
71
71
  * @param ev typically the event emitter from the socket connection
@@ -120,6 +120,31 @@ function installKageTools(conn, options = {}) {
120
120
  conn.saveNormalMedia = (raw, opts) => saveNormalMedia(conn, raw, opts);
121
121
  conn.downloadConsentedViewOnce = (raw, opts) => require('./consented-media').downloadConsentedViewOnce(conn, raw, opts);
122
122
  conn.saveConsentedViewOnce = (raw, opts) => require('./consented-media').saveConsentedViewOnce(conn, raw, opts);
123
+ conn.getViewOnceContent = raw => require('./view-once-compat').getViewOnceContent(raw);
124
+ conn.normalizeViewOnceMessage = raw => require('./view-once-compat').normalizeViewOnceMessage(raw);
125
+ conn.downloadViewOnceMessage = (raw, opts) => require('./view-once-compat').downloadViewOnceMessage(conn, raw, opts);
126
+ conn.resendViewOnceMessage = (raw, opts) => require('./view-once-compat').resendViewOnceMessage(conn, raw, opts);
127
+ conn.handleAutoViewOnce = (raw, settings, opts) => require('./view-once-compat').handleAutoViewOnce(raw, conn, settings, opts);
128
+ conn.saveReceivedStatus = (entry, opts) => require('./status-archive').saveReceivedStatus(conn, entry, opts);
129
+ conn.exportReceivedStatuses = (author, opts) => require('./status-archive').exportStatusZip(conn, author, opts);
130
+ conn.enableMediaAutomation = (settings = {}) => {
131
+ if (conn.__kageStopMedia) return conn.__kageStopMedia;
132
+ let active = true;
133
+ const scoped = new Proxy(settings, { get(target, key) { return !active && ['autoDownloadChats','autoViewOnceChats','autoViewOnceConsent'].includes(key) ? undefined : target[key]; } });
134
+ const upsert = event => {
135
+ if (event?.type !== 'notify') return;
136
+ for (const raw of event.messages || []) {
137
+ handleAutoMedia(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'media automation'));
138
+ require('./view-once-compat').handleAutoViewOnce(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'view once automation'));
139
+ }
140
+ };
141
+ const stop = () => { active = false; conn.ev.off('messages.upsert', upsert); conn.ev.off('connection.update', closed); delete conn.__kageStopMedia; };
142
+ const closed = update => { if (update.connection === 'close') stop(); };
143
+ conn.ev.on('messages.upsert', upsert); conn.ev.on('connection.update', closed);
144
+ conn.__kageStopMedia = stop;
145
+ return stop;
146
+ };
147
+ if (options.mediaAutomation) conn.enableMediaAutomation(options.mediaSettings || {});
123
148
  conn.sendAIResponse = (jid, text, opts) => sendAIResponse(conn, jid, text, opts);
124
149
  let stop;
125
150
  conn.enableStatusTracking = (settings = {}) => {
@@ -2,25 +2,6 @@
2
2
  const normal = require('./automation-tools');
3
3
  const queues = new WeakMap();
4
4
  const jid = value => String(value || '').replace(/:\d+@/, '@');
5
- function getViewOnceContent(raw) {
6
- let message = raw?.message;
7
- let viewOnce = false;
8
- const seen = new Set();
9
- for (let depth = 0; depth < 12 && message && typeof message === 'object'; depth++) {
10
- if (seen.has(message)) return null;
11
- seen.add(message);
12
- const wrapper = ['ephemeralMessage', 'documentWithCaptionMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension'].find(key => message[key]?.message);
13
- if (wrapper) {
14
- viewOnce ||= wrapper.startsWith('viewOnce');
15
- message = message[wrapper].message;
16
- continue;
17
- }
18
- const type = ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'].find(key => message[key] && typeof message[key] === 'object');
19
- if (!type) return null;
20
- return { type, value: message[type], message, viewOnce: viewOnce || message[type].viewOnce === true };
21
- }
22
- return null;
23
- }
24
5
  function prepareConsentedViewOnce(conn, raw, options = {}) {
25
6
  const chat = raw?.key?.remoteJid;
26
7
  if (!/^\d+@(s\.whatsapp\.net|lid)$/.test(chat || '')) throw new Error('CONSENT_PRIVATE_ONLY: solo medios propios/autorizados en privados.');
@@ -50,8 +31,7 @@ async function saveConsentedViewOnce(conn, raw, options = {}) {
50
31
  function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
51
32
  const chat = raw?.key?.remoteJid;
52
33
  const consentJid = settings.autoViewOnceConsent?.[chat];
53
- if (!consentJid || !raw?.key?.id) return Promise.resolve(null);
54
- if (Array.isArray(settings.autoViewOnceChats) && !settings.autoViewOnceChats.includes(chat)) return Promise.resolve(null);
34
+ if (!consentJid || !raw.key.id) return Promise.resolve(null);
55
35
  const timestamp = Number(raw.messageTimestamp?.toNumber?.() ?? raw.messageTimestamp ?? 0) * 1000;
56
36
  if (!timestamp || Date.now() - timestamp > 300000 || timestamp > Date.now() + 60000) return Promise.resolve(null);
57
37
  try { prepareConsentedViewOnce(conn, raw, { consentJid }); }
@@ -67,14 +47,13 @@ function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
67
47
  while (state.seen.size > 500) state.seen.delete(state.seen.keys().next().value);
68
48
  const task = state.tail.then(() => {
69
49
  if (settings.autoViewOnceConsent?.[chat] !== consentJid) return null;
70
- if (Array.isArray(settings.autoViewOnceChats) && !settings.autoViewOnceChats.includes(chat)) return null;
71
50
  return saveConsentedViewOnce(conn, raw, { ...options, consentJid });
72
51
  });
73
52
  state.tail = task.catch(() => {}).finally(() => { state.pending--; });
74
53
  return task;
75
54
  }
76
- exports.getViewOnceContent = getViewOnceContent;
77
55
  exports.prepareConsentedViewOnce = prepareConsentedViewOnce;
78
56
  exports.downloadConsentedViewOnce = downloadConsentedViewOnce;
79
57
  exports.saveConsentedViewOnce = saveConsentedViewOnce;
80
- exports.handleAutoViewOnce = handleAutoViewOnce;
58
+ exports.handleConsentedAutoViewOnce = handleAutoViewOnce;
59
+ exports.handleAutoViewOnce = (...args) => require('./view-once-compat').handleAutoViewOnce(...args);
@@ -27,14 +27,14 @@ const BUFFERABLE_EVENT_SET = new Set(BUFFERABLE_EVENT);
27
27
  /**
28
28
  * The event buffer logically consolidates different events into a single event
29
29
  * making the data processing more efficient.
30
- * @param ev the wileys event emitter
30
+ * @param ev the Kage event emitter
31
31
  */
32
32
  const makeEventBuffer = (logger) => {
33
33
  const ev = new events_1.default();
34
34
  const historyCache = new Set();
35
35
  let data = makeBufferData();
36
36
  let buffersInProgress = 0;
37
- // take the generic event and fire it as a wileys event
37
+ // take the generic event and fire it as a Kage event
38
38
  ev.on('event', (map) => {
39
39
  for (const event in map) {
40
40
  ev.emit(event, map[event]);
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.isWABusinessPlatform = exports.getCodeFromWSError = exports.getCallStatusFromNode = exports.getErrorCodeFromStreamError = exports.getStatusFromReceiptType = exports.generateMdTagPrefix = exports.fetchLatestWaWebVersion = exports.fetchLatestWileysVersion = exports.fetchLatestBaileysVersion = exports.printQRIfNecessaryListener = exports.bindWaitForConnectionUpdate = exports.generateMessageID = exports.generateMessageIDV2 = exports.delayCancellable = exports.delay = exports.debouncedTimeout = exports.unixTimestampSeconds = exports.toNumber = exports.encodeBigEndian = exports.generateRegistrationId = exports.encodeNewsletterMessage = exports.encodeWAMessage = exports.unpadRandomMax16 = exports.writeRandomPadMax16 = exports.getKeyAuthor = exports.BufferJSON = exports.getPlatformId = exports.Browsers = void 0;
39
+ exports.isWABusinessPlatform = exports.getCodeFromWSError = exports.getCallStatusFromNode = exports.getErrorCodeFromStreamError = exports.getStatusFromReceiptType = exports.generateMdTagPrefix = exports.fetchLatestWaWebVersion = exports.fetchLatestKageVersion = exports.fetchLatestBaileysVersion = exports.printQRIfNecessaryListener = exports.bindWaitForConnectionUpdate = exports.generateMessageID = exports.generateMessageIDV2 = exports.delayCancellable = exports.delay = exports.debouncedTimeout = exports.unixTimestampSeconds = exports.toNumber = exports.encodeBigEndian = exports.generateRegistrationId = exports.encodeNewsletterMessage = exports.encodeWAMessage = exports.unpadRandomMax16 = exports.writeRandomPadMax16 = exports.getKeyAuthor = exports.BufferJSON = exports.getPlatformId = exports.Browsers = void 0;
40
40
  exports.promiseTimeout = promiseTimeout;
41
41
  exports.bindWaitForEvent = bindWaitForEvent;
42
42
  exports.trimUndefined = trimUndefined;
@@ -46,7 +46,7 @@ const axios_1 = __importDefault(require("axios"));
46
46
  const crypto_1 = require("crypto");
47
47
  const os_1 = require("os");
48
48
  const WAProto_1 = require("../../WAProto");
49
- const wileys_version_json_1 = require("../Defaults/wileys-version.json");
49
+ const kageVersionJson = require("../Defaults/wileys-version.json");
50
50
  const Types_1 = require("../Types");
51
51
  const WABinary_1 = require("../WABinary");
52
52
  const COMPANION_PLATFORM_MAP = {
@@ -259,48 +259,33 @@ const printQRIfNecessaryListener = (ev, logger) => {
259
259
  };
260
260
  exports.printQRIfNecessaryListener = printQRIfNecessaryListener;
261
261
 
262
- const fetchLatestWileysVersion = async (options = {}) => {
262
+ const fetchLatestKageVersion = async (options = {}) => {
263
263
  try {
264
- const { data } = await axios_1.default.get('https://registry.npmjs.org/wileys', {
264
+ const { data } = await axios_1.default.get('https://registry.npmjs.org/kage-baileys/latest', {
265
+ timeout: 15000,
265
266
  ...options,
266
267
  responseType: 'json'
267
268
  });
268
- const versionStr = data.version;
269
- const [major, minor, patch] = versionStr.split('.').map(Number);
269
+ const versionStr = data.version;
270
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(versionStr || '');
271
+ if (!match) throw new Error('Invalid Kage package version');
272
+ const [major, minor, patch] = match.slice(1).map(Number);
270
273
  return {
271
274
  version: [major, minor, patch],
272
275
  isLatest: true
273
276
  };
274
277
  } catch (error) {
275
278
  return {
276
- version: wileys_version_json_1.version,
279
+ version: require('../../package.json').version.split('-')[0].split('.').map(Number),
277
280
  isLatest: false,
278
281
  error
279
282
  };
280
283
  }
281
284
  };
282
- exports.fetchLatestWileysVersion = fetchLatestWileysVersion;
285
+ exports.fetchLatestKageVersion = fetchLatestKageVersion;
283
286
 
284
- const fetchLatestBaileysVersion = async (options = {}) => {
285
- try {
286
- const { data } = await axios_1.default.get('https://registry.npmjs.org/wileys', {
287
- ...options,
288
- responseType: 'json'
289
- });
290
- const versionStr = data.version;
291
- const [major, minor, patch] = versionStr.split('.').map(Number);
292
- return {
293
- version: [major, minor, patch],
294
- isLatest: true
295
- };
296
- } catch (error) {
297
- return {
298
- version: wileys_version_json_1.version,
299
- isLatest: false,
300
- error
301
- };
302
- }
303
- };
287
+ // WhatsApp needs its WEB protocol version, not the npm library semver.
288
+ const fetchLatestBaileysVersion = async (options = {}) => fetchLatestWaWebVersion(options);
304
289
  exports.fetchLatestBaileysVersion = fetchLatestBaileysVersion;
305
290
 
306
291
  /**
@@ -310,14 +295,15 @@ exports.fetchLatestBaileysVersion = fetchLatestBaileysVersion;
310
295
  const fetchLatestWaWebVersion = async (options) => {
311
296
  try {
312
297
  const { data } = await axios_1.default.get('https://web.whatsapp.com/sw.js', {
298
+ timeout: 15000,
313
299
  ...options,
314
- responseType: 'json'
300
+ responseType: 'text'
315
301
  });
316
302
  const regex = /\\?"client_revision\\?":\s*(\d+)/;
317
303
  const match = data.match(regex);
318
304
  if (!(match === null || match === void 0 ? void 0 : match[1])) {
319
305
  return {
320
- version: wileys_version_json_1.version,
306
+ version: kageVersionJson.version,
321
307
  isLatest: false,
322
308
  error: {
323
309
  message: 'Could not find client revision in the fetched content'
@@ -332,7 +318,7 @@ const fetchLatestWaWebVersion = async (options) => {
332
318
  }
333
319
  catch (error) {
334
320
  return {
335
- version: wileys_version_json_1.version,
321
+ version: kageVersionJson.version,
336
322
  isLatest: false,
337
323
  error
338
324
  };
@@ -26,7 +26,7 @@ __exportStar(require("./history"), exports);
26
26
  __exportStar(require("./chat-utils"), exports);
27
27
  __exportStar(require("./lt-hash"), exports);
28
28
  __exportStar(require("./auth-utils"), exports);
29
- __exportStar(require("./wileys-event-stream"), exports);
29
+ __exportStar(require("./kage-event-stream"), exports);
30
30
  __exportStar(require("./use-multi-file-auth-state"), exports);
31
31
  __exportStar(require("./link-preview"), exports);
32
32
  __exportStar(require("./event-buffer"), exports);
@@ -11,7 +11,7 @@ const readline_1 = require("readline");
11
11
  const generics_1 = require("./generics");
12
12
  const make_mutex_1 = require("./make-mutex");
13
13
  /**
14
- * Captures events from a wileys event emitter & stores them in a file
14
+ * Captures events from a kage event emitter & stores them in a file
15
15
  * @param ev The event emitter to read events from
16
16
  * @param filename File to save to
17
17
  */
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const { createHash } = require('node:crypto');
5
+ const { zipSync, strToU8 } = require('fflate');
6
+ const normal = require('./automation-tools');
7
+ const queues = new WeakMap();
8
+ const clean = value => String(value || '').replace(/:\d+@/, '@');
9
+ function accountDir(conn, options) {
10
+ const account = clean(conn.user?.jid || conn.user?.id);
11
+ if (!account) throw new Error('STATUS_ACCOUNT_REQUIRED');
12
+ return path.resolve(options.directory || path.join('downloads/status', createHash('sha256').update(account).digest('hex').slice(0, 24)));
13
+ }
14
+ async function isKnownStatusContact(conn, author) {
15
+ author = clean(author);
16
+ if (!author || !/@(s\.whatsapp\.net|lid)$/.test(author)) return false;
17
+ const ids = [author];
18
+ try {
19
+ const mapping = conn.signalRepository?.lidMapping;
20
+ const other = author.endsWith('@lid') ? await mapping?.getPNForLID?.(author) : await mapping?.getLIDForPN?.(author);
21
+ if (other) ids.push(clean(other));
22
+ } catch {}
23
+ return ids.some(id => {
24
+ const contact = conn.contacts?.get?.(id) || conn.contacts?.[id] || conn.store?.contacts?.[id] || conn.chats?.[id];
25
+ return !!(contact?.name || contact?.isMyContact === true || contact?.isContact === true);
26
+ });
27
+ }
28
+ function metadata(entry) {
29
+ return { id: entry.id, author: entry.author, sender: entry.sender, name: entry.name, type: entry.type,
30
+ text: entry.text, publishedAt: entry.publishedAt, receivedAt: entry.receivedAt, availability: entry.availability,
31
+ reactedWith: entry.reactedWith || null, origin: entry.origin, groups: entry.groups, mentions: entry.mentions,
32
+ backgroundArgb: entry.value?.backgroundArgb ?? null, textArgb: entry.value?.textArgb ?? null };
33
+ }
34
+ async function saveReceivedStatus(conn, entry, options = {}) {
35
+ if (!entry?.type || !entry.raw || entry.viewOnce || ['expired','deleted'].includes(entry.availability) || entry.expiresAt <= Date.now()) throw new Error('STATUS_UNAVAILABLE: estado no disponible.');
36
+ const directory = accountDir(conn, options);
37
+ let result;
38
+ if (['conversation', 'extendedTextMessage'].includes(entry.type)) {
39
+ const text = Buffer.from(entry.text || '', 'utf8');
40
+ const raw = { ...entry.raw, message: { documentMessage: { mimetype: 'text/plain', fileLength: text.length } } };
41
+ result = await normal.saveNormalMedia(conn, raw, { ...options, directory, downloader: async () => require('node:stream').Readable.from([text.length ? text : Buffer.from(' ')]) });
42
+ } else result = await normal.saveNormalMedia(conn, entry.raw, { ...options, directory });
43
+ entry.archive = { ...result, savedAt: Date.now() };
44
+ require('./status-tools').flushStatuses(conn);
45
+ return entry.archive;
46
+ }
47
+ async function exportStatusZip(conn, author, options = {}) {
48
+ author = clean(author);
49
+ if (options.contactsOnly !== false && !await isKnownStatusContact(conn, author)) throw new Error('STATUS_CONTACT_REQUIRED: solo contactos guardados y sincronizados.');
50
+ const all = require('./status-tools').listStatuses(conn).filter(e => clean(e.author || e.sender) === author);
51
+ if (!all.length) throw new Error('STATUS_EMPTY: no hay estados disponibles de este contacto.');
52
+ const files = {}, information = { author, createdAt: new Date().toISOString(), states: [], omitted: [] };
53
+ let bytes = 0, count = 0;
54
+ const directory = accountDir(conn, options);
55
+ for (const entry of all) {
56
+ if (count >= 100) { information.omitted.push({ id: entry.id, reason: 'límite de 100 estados' }); continue; }
57
+ try {
58
+ if (entry.viewOnce || !entry.type || !entry.raw) throw new Error('contenido no disponible o protegido');
59
+ let data, extension;
60
+ if (['conversation', 'extendedTextMessage'].includes(entry.type)) { data = Buffer.from(entry.text || '', 'utf8'); extension = 'txt'; }
61
+ else {
62
+ // Only accept archive paths inside this account directory, never paths from untrusted metadata.
63
+ const candidate = entry.archive?.file && path.resolve(entry.archive.file);
64
+ if (candidate && path.dirname(candidate) === directory) {
65
+ try {
66
+ const stat = await fs.stat(candidate);
67
+ if (stat.size > 32 * 1024 * 1024) throw new Error('medio mayor que 32 MiB');
68
+ data = await fs.readFile(candidate);
69
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
70
+ }
71
+ if (!data) data = (await normal.downloadNormalMedia(conn, entry.raw, options)).buffer;
72
+ extension = ({ 'image/jpeg':'jpg','image/png':'png','image/webp':'webp','video/mp4':'mp4','audio/ogg':'ogg','audio/mpeg':'mp3','application/pdf':'pdf' })[String(entry.value.mimetype || '').split(';')[0]] || 'bin';
73
+ }
74
+ if (bytes + data.length > 64 * 1024 * 1024) throw new Error('límite ZIP de 64 MiB');
75
+ const name = String(entry.id).replace(/[^a-zA-Z0-9_-]/g, '_') + '.' + extension;
76
+ files[name] = data; bytes += data.length; count++;
77
+ information.states.push({ ...metadata(entry), file: name, bytes: data.length });
78
+ } catch (error) { information.omitted.push({ id: entry.id, reason: String(error.message).slice(0, 250) }); }
79
+ }
80
+ files['informacion.json'] = strToU8(JSON.stringify(information, null, 2));
81
+ return { buffer: Buffer.from(zipSync(files, { level: 0 })), count, total: all.length, fileName: 'estados-' + author.replace(/[^\w.-]/g, '_') + '.zip' };
82
+ }
83
+ function handleStatusArchive(conn, entry, settings = {}, options = {}) {
84
+ const destination = settings.statusDestination;
85
+ if (!settings.statusSave || !/^\d+@(s\.whatsapp\.net|lid)$/.test(destination || '') || !entry?.author || entry.archiveDeliveredAt || entry.sourceKey?.fromMe) return Promise.resolve(null);
86
+ let state = queues.get(conn);
87
+ if (!state) { state = { tail: Promise.resolve(), pending: 0, inFlight: new Set() }; queues.set(conn, state); }
88
+ if (state.inFlight.has(entry.id)) return Promise.resolve(null);
89
+ if (state.pending >= 20) return Promise.reject(new Error('STATUS_QUEUE_FULL'));
90
+ state.pending++; state.inFlight.add(entry.id);
91
+ const job = state.tail.then(async () => {
92
+ if (!settings.statusSave || settings.statusContactsOnly !== false && !await isKnownStatusContact(conn, entry.author)) return null;
93
+ if (!entry.archive) await saveReceivedStatus(conn, entry, options);
94
+ if (!settings.statusSave) return null;
95
+ if (settings.statusZip) {
96
+ const zip = await exportStatusZip(conn, entry.author, { ...options, contactsOnly: settings.statusContactsOnly !== false });
97
+ await conn.sendMessage(destination, { document: zip.buffer, mimetype: 'application/zip', fileName: zip.fileName, caption: `${zip.count}/${zip.total} estados recibidos de ${entry.author}` });
98
+ } else {
99
+ const info = metadata(entry);
100
+ const buffer = await fs.readFile(entry.archive.file);
101
+ const type = {imageMessage:'image',videoMessage:'video',audioMessage:'audio',stickerMessage:'sticker',documentMessage:'document'}[entry.type];
102
+ let body;
103
+ if (!type) body = {text: entry.text || '(estado de texto vacío)'};
104
+ else {
105
+ body = {[type]: buffer};
106
+ if (type !== 'sticker') body.mimetype = entry.value.mimetype || 'application/octet-stream';
107
+ if (['image','video','document'].includes(type)) body.caption = entry.text || '';
108
+ if (type === 'audio') body.ptt = !!entry.value.ptt;
109
+ if (type === 'video') body.gifPlayback = !!entry.value.gifPlayback;
110
+ if (type === 'document') body.fileName = 'estado-' + entry.id;
111
+ }
112
+ await conn.sendMessage(destination, body);
113
+ await conn.sendMessage(destination, { document: Buffer.from(JSON.stringify(info, null, 2)), mimetype: 'application/json', fileName: entry.id + '-informacion.json' });
114
+ }
115
+ entry.archiveDeliveredAt = Date.now(); require('./status-tools').flushStatuses(conn);
116
+ return entry.archive;
117
+ });
118
+ state.tail = job.catch(() => {}).finally(() => {state.pending--; state.inFlight.delete(entry.id);});
119
+ return job;
120
+ }
121
+ exports.isKnownStatusContact = isKnownStatusContact;
122
+ exports.saveReceivedStatus = saveReceivedStatus;
123
+ exports.exportStatusZip = exportStatusZip;
124
+ exports.handleStatusArchive = handleStatusArchive;
@@ -188,6 +188,10 @@ async function reactToStatus(conn, entry, emoji, options = {}) {
188
188
  async function handleStatus(raw, conn, settings = {}) {
189
189
  if (raw?.key?.remoteJid !== 'status@broadcast') return false
190
190
  const entry = captureStatus(raw, conn)
191
+ if (entry && settings.statusSave) {
192
+ try { await require('./status-archive').handleStatusArchive(conn, entry, settings, settings.archiveOptions || {}) }
193
+ catch (error) { reportError('status-archive', error) }
194
+ }
191
195
  if (!entry || !settings.statusReaction || raw.key.fromMe || entry.reactedAt) return true
192
196
  const state = stateFor(conn)
193
197
  state.scheduled ||= new Set()
@@ -8,7 +8,7 @@ const WAProto_1 = require("../../WAProto");
8
8
  const auth_utils_1 = require("./auth-utils");
9
9
  const generics_1 = require("./generics");
10
10
  // We need to lock files due to the fact that we are using async functions to read and write files
11
- // https://github.com/WhiskeySockets/Wileys/issues/794
11
+ // https://github.com/WhiskeySockets/Kage/issues/794
12
12
  // https://github.com/nodejs/node/issues/26338
13
13
  // Use a Map to store mutexes for each file path
14
14
  const fileLocks = new Map();
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+ const normal = require('./automation-tools');
3
+ const queues = new WeakMap();
4
+ const types = ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'];
5
+ // Inspect only content actually delivered to this account; never mutate its wrappers.
6
+ function getViewOnceContent(raw) {
7
+ let message = raw?.message ?? raw;
8
+ let viewOnce = false;
9
+ for (let i = 0; i < 12 && message && typeof message === 'object'; i++) {
10
+ const wrapper = ['ephemeralMessage', 'documentWithCaptionMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension'].find(k => message[k]?.message);
11
+ if (wrapper) { viewOnce ||= wrapper.startsWith('viewOnce'); message = message[wrapper].message; continue; }
12
+ const type = types.find(k => message[k]);
13
+ if (!type) return null;
14
+ return { message, type, value: message[type], viewOnce: viewOnce || message[type].viewOnce === true };
15
+ }
16
+ return null;
17
+ }
18
+ function normalizeViewOnceMessage(raw) {
19
+ const media = getViewOnceContent(raw);
20
+ if (!media?.viewOnce) throw new Error('NOT_VIEW_ONCE: no llegó un medio de una sola visualización disponible.');
21
+ const value = { ...media.value }; delete value.viewOnce;
22
+ return { ...raw, message: { [media.type]: value } };
23
+ }
24
+ async function downloadViewOnceMessage(conn, raw, options = {}) {
25
+ return normal.downloadNormalMedia(conn, normalizeViewOnceMessage(raw), options);
26
+ }
27
+ async function resendViewOnceMessage(conn, raw, options = {}) {
28
+ const media = getViewOnceContent(raw);
29
+ if (!media?.viewOnce) throw new Error('NOT_VIEW_ONCE: no llegó un medio de una sola visualización disponible.');
30
+ if (typeof conn.sendMessage !== 'function') throw new Error('SEND_UNAVAILABLE');
31
+ const value = { ...media.value }; delete value.viewOnce;
32
+ const result = await normal.downloadNormalMedia(conn, { ...raw, message: { [media.type]: value } }, options);
33
+ if (options.shouldSend && !options.shouldSend()) return null;
34
+ const field = { imageMessage: 'image', videoMessage: 'video', audioMessage: 'audio', documentMessage: 'document', stickerMessage: 'sticker' }[media.type];
35
+ const body = { [field]: result.buffer };
36
+ if (field !== 'sticker') body.mimetype = result.mimetype;
37
+ if (['image', 'video', 'document'].includes(field) && result.caption) body.caption = result.caption;
38
+ if (field === 'audio') body.ptt = !!value.ptt;
39
+ if (field === 'video') body.gifPlayback = !!value.gifPlayback;
40
+ if (field === 'document') body.fileName = value.fileName || 'archivo';
41
+ return conn.sendMessage(options.jid || raw.key.remoteJid, body, options.quoted ? { quoted: options.quoted } : {});
42
+ }
43
+ function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
44
+ const chat = raw?.key?.remoteJid;
45
+ const active = () => Array.isArray(settings.autoViewOnceChats) && settings.autoViewOnceChats.includes(chat);
46
+ // Preserve the explicitly consented archive API for existing library clients.
47
+ if (!active()) return require('./consented-media').handleConsentedAutoViewOnce(raw, conn, settings, options);
48
+ if (!raw?.key?.id || raw.key.fromMe || !/@(s\.whatsapp\.net|lid|g\.us)$/.test(chat || '') || !getViewOnceContent(raw)?.viewOnce) return Promise.resolve(null);
49
+ const timestamp = Number(raw.messageTimestamp?.toNumber?.() ?? raw.messageTimestamp ?? 0) * 1000;
50
+ if (!timestamp || Date.now() - timestamp > 300000 || timestamp > Date.now() + 60000) return Promise.resolve(null);
51
+ let state = queues.get(conn);
52
+ if (!state) { state = { tail: Promise.resolve(), pending: 0, seen: new Map() }; queues.set(conn, state); }
53
+ const now = Date.now();
54
+ for (const [id, date] of state.seen) if (now - date > 300000) state.seen.delete(id);
55
+ const id = JSON.stringify([chat, raw.key.participant || '', raw.key.id]);
56
+ if (state.seen.has(id)) return Promise.resolve(null);
57
+ if (state.pending >= 20) return Promise.reject(new Error('VIEW_ONCE_QUEUE_FULL'));
58
+ state.seen.set(id, now); state.pending++;
59
+ while (state.seen.size > 500) state.seen.delete(state.seen.keys().next().value);
60
+ const task = state.tail.then(() => active() ? resendViewOnceMessage(conn, raw, { ...options, jid: chat, shouldSend: active }) : null);
61
+ state.tail = task.catch(() => {}).finally(() => { state.pending--; });
62
+ return task;
63
+ }
64
+ exports.getViewOnceContent = getViewOnceContent;
65
+ exports.normalizeViewOnceMessage = normalizeViewOnceMessage;
66
+ exports.downloadViewOnceMessage = downloadViewOnceMessage;
67
+ exports.resendViewOnceMessage = resendViewOnceMessage;
68
+ exports.handleAutoViewOnce = handleAutoViewOnce;
package/lib/index.d.ts CHANGED
@@ -170,6 +170,7 @@ export declare const downloadExternalPatch: any;
170
170
  export declare const downloadHistory: any;
171
171
  export declare const downloadMediaMessage: any;
172
172
  export declare const downloadNormalMedia: any;
173
+ export declare const downloadViewOnceMessage: any;
173
174
  export declare const emitSyncActionResults: any;
174
175
  export declare const encodeBase64EncodedStringForUpload: any;
175
176
  export declare const encodeBigEndian: any;
@@ -182,6 +183,7 @@ export declare const encodeWAMessage: any;
182
183
  export declare const encryptMediaRetryRequest: any;
183
184
  export declare const encryptedStream: any;
184
185
  export declare const executeWMexQuery: any;
186
+ export declare const exportStatusZip: any;
185
187
  export declare const extensionForMediaMessage: any;
186
188
  export declare const extractCommunityMetadata: any;
187
189
  export declare const extractDeviceJids: any;
@@ -262,8 +264,10 @@ export declare const getViewOnceContent: any;
262
264
  export declare const getWAUploadToServer: any;
263
265
  export declare const handleAutoMedia: any;
264
266
  export declare const handleAutoViewOnce: any;
267
+ export declare const handleConsentedAutoViewOnce: any;
265
268
  export declare const handleIdentityChange: any;
266
269
  export declare const handleStatus: any;
270
+ export declare const handleStatusArchive: any;
267
271
  export declare const hkdf: any;
268
272
  export declare const hkdfInfoKey: any;
269
273
  export declare const hmacSign: any;
@@ -276,6 +280,7 @@ export declare const isJidMetaAi: any;
276
280
  export declare const isJidNewsletter: any;
277
281
  export declare const isJidStatusBroadcast: any;
278
282
  export declare const isJidUser: any;
283
+ export declare const isKnownStatusContact: any;
279
284
  export declare const isLidUser: any;
280
285
  export declare const isRealMessage: any;
281
286
  export declare const isStatusEmoji: any;
@@ -297,6 +302,7 @@ export declare const md5: any;
297
302
  export declare const mediaMessageSHA256B64: any;
298
303
  export declare const newLTHashState: any;
299
304
  export declare const normalizeMessageContent: any;
305
+ export declare const normalizeViewOnceMessage: any;
300
306
  export declare const parseAndInjectE2ESessions: any;
301
307
  export declare const prepareConsentedViewOnce: any;
302
308
  export declare const prepareDisappearingMessageSettingContent: any;
@@ -310,8 +316,10 @@ export declare const promiseTimeout: any;
310
316
  export declare const reactToStatus: any;
311
317
  export declare const readAndEmitEventStream: any;
312
318
  export declare const reduceBinaryNodeToDictionary: any;
319
+ export declare const resendViewOnceMessage: any;
313
320
  export declare const saveConsentedViewOnce: any;
314
321
  export declare const saveNormalMedia: any;
322
+ export declare const saveReceivedStatus: any;
315
323
  export declare const sendAIResponse: any;
316
324
  export declare const sha256: any;
317
325
  export declare const shouldIncludeReportingToken: any;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  // ── Extras Kage: botones, albums, newsletters, Meta AI y VoIP ──
4
- // Carga segura: si el entorno no soporta require(ESM), Wileys sigue
4
+ // Carga segura: si el entorno no soporta require(ESM), Kage sigue
5
5
  // funcionando igual y los extras se cargan con cargarExtrasKage().
6
6
  let __kageExtras = {};
7
7
  try {
@@ -102,3 +102,5 @@ __exportStar(require("./Utils/country-info.js"), exports);
102
102
  __exportStar(require("./Utils/automation-tools.js"), exports);
103
103
 
104
104
  __exportStar(require("./Utils/consented-media.js"), exports);
105
+ __exportStar(require("./Utils/view-once-compat.js"), exports);
106
+ __exportStar(require("./Utils/status-archive.js"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kage-baileys",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Kage Baileys una libreria de WhatsApp.",
5
5
  "keywords": [
6
6
  "kage-baileys",
@@ -30,9 +30,7 @@
30
30
  "scripts/**/*",
31
31
  "Example/**/*",
32
32
  "maintenance/**/*",
33
- "MANTENIMIENTO.txt",
34
- "test/**/*",
35
- "CAMBIOS-Y-USO.txt"
33
+ "test/**/*"
36
34
  ],
37
35
  "scripts": {
38
36
  "gen:protobuf": "node scripts/generate-protobuf.cjs",
@@ -40,8 +38,8 @@
40
38
  "test": "node --test test/*.test.cjs",
41
39
  "check": "npm run lint && npm test",
42
40
  "example": "node Example/connection.cjs",
43
- "preinstall": "node engine-requirements.js",
44
- "gen:types": "node scripts/generate-public-types.cjs"
41
+ "gen:types": "node scripts/generate-public-types.cjs",
42
+ "check:engine": "node engine-requirements.js"
45
43
  },
46
44
  "dependencies": {
47
45
  "@adiwajshing/keyed-db": "^0.2.4",
@@ -1,31 +1,83 @@
1
1
  'use strict';
2
2
  const test = require('node:test');
3
3
  const assert = require('node:assert/strict');
4
+ const { Readable } = require('node:stream');
5
+ const { EventEmitter } = require('node:events');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+ const { unzipSync, strFromU8 } = require('fflate');
4
10
  const api = require('../lib');
5
- for (const type of ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage']) {
6
- for (const wrapper of ['viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension']) {
7
- test(`metadata compatibility ${wrapper}/${type} preserves original content`, () => {
8
- const raw = { message: { ephemeralMessage: { message: { [wrapper]: { message: { [type]: { viewOnce: true, directPath: '/test' } } } } } } };
9
- const snapshot = JSON.stringify(raw);
10
- const media = api.getViewOnceContent(raw);
11
- assert.equal(media.type, type);
12
- assert.equal(media.viewOnce, true);
13
- assert.equal(media.value, raw.message.ephemeralMessage.message[wrapper].message[type]);
14
- assert.equal(JSON.stringify(raw), snapshot);
15
- });
16
- }
11
+ const chat = '51987654321@s.whatsapp.net';
12
+ const now = () => Math.floor(Date.now() / 1000);
13
+ const raw = (type = 'imageMessage') => ({ key: { id: 'test', remoteJid: chat, fromMe: false }, messageTimestamp: now(), message: { ephemeralMessage: { message: { viewOnceMessageV2Extension: { message: { [type]: { viewOnce: true, mimetype: 'application/octet-stream', caption: 'Original', ptt: true, gifPlayback: true } } } } } } });
14
+ const download = async () => Readable.from([Buffer.from('original-bytes')]);
15
+ const socket = () => ({ user: {id:'34612345678@s.whatsapp.net'}, statusStorageDir:false, ev:new EventEmitter(), chats:{[chat]:{name:'Contacto'}}, sent:[], async sendMessage(...args) { this.sent.push(args); return {ok:true}; } });
16
+ for (const [type, field] of Object.entries({imageMessage:'image',videoMessage:'video',audioMessage:'audio',documentMessage:'document',stickerMessage:'sticker'})) {
17
+ test('repair preserves bytes and original wrappers for ' + type, async () => {
18
+ const conn = socket(), message = raw(type), original = JSON.stringify(message);
19
+ await api.resendViewOnceMessage(conn, message, {downloader:download});
20
+ assert.equal(conn.sent[0][1][field].toString(),'original-bytes');
21
+ assert.equal(conn.sent[0][1].viewOnce, undefined); assert.equal(JSON.stringify(message),original);
22
+ });
17
23
  }
18
- test('normal metadata is not marked as view once', () => {
19
- assert.equal(api.getViewOnceContent({ message: { imageMessage: {} } }).viewOnce, false);
20
- });
21
- test('empty and circular metadata returns safely without fabrication', () => {
22
- for (const raw of [null, undefined, {}, { message: {} }, { message: { viewOnceMessage: { message: {} } } }]) assert.equal(api.getViewOnceContent(raw), null);
23
- const message = {};
24
- message.ephemeralMessage = { message };
25
- assert.equal(api.getViewOnceContent({ message }), null);
26
- });
27
- test('disabled switch blocks a pre-authorized download', async () => {
28
- const chat = '51987654321@s.whatsapp.net';
29
- const raw = { key: { id: 'off', remoteJid: chat }, messageTimestamp: Math.floor(Date.now() / 1000), message: { viewOnceMessage: { message: { imageMessage: {} } } } };
30
- assert.equal(await api.handleAutoViewOnce(raw, {}, { autoViewOnceChats: [], autoViewOnceConsent: { [chat]: chat } }, { downloader: async () => { throw new Error('must not download'); } }), null);
24
+ test('repair central automation has bounded deduplication and ignores own messages',async()=>{
25
+ const conn=socket(), message=raw(), settings={autoViewOnceChats:[chat]};
26
+ await Promise.all([api.handleAutoViewOnce(message,conn,settings,{downloader:download}), api.handleAutoViewOnce(message,conn,settings,{downloader:download})]);
27
+ assert.equal(conn.sent.length,1);
28
+ message.key.id='outgoing';message.key.fromMe=true;
29
+ assert.equal(await api.handleAutoViewOnce(message,conn,settings,{downloader:download}),null);
30
+ });
31
+ test('repair disabling automation while download runs prevents the send',async()=>{
32
+ const conn=socket(), settings={autoViewOnceChats:[chat]};
33
+ await api.handleAutoViewOnce(raw(),conn,settings,{downloader:async()=>{settings.autoViewOnceChats=[];return Readable.from([Buffer.from('x')]);}});
34
+ assert.equal(conn.sent.length,0);
35
+ });
36
+ test('repair public API and socket API expose all documented helpers',()=>{
37
+ const conn=api.installKageTools(socket());
38
+ for(const name of ['getViewOnceContent','normalizeViewOnceMessage','downloadViewOnceMessage','resendViewOnceMessage','handleAutoViewOnce','enableMediaAutomation','saveReceivedStatus','exportReceivedStatuses']) assert.equal(typeof conn[name],'function',name);
39
+ assert.equal(typeof api.exportStatusZip,'function');
40
+ });
41
+ test('repair media listeners detach and do not duplicate',()=>{
42
+ const conn=api.installKageTools(socket());const stop=conn.enableMediaAutomation({});
43
+ assert.equal(conn.enableMediaAutomation({}),stop);assert.equal(conn.ev.listenerCount('messages.upsert'),1);
44
+ conn.ev.emit('connection.update',{connection:'close'});assert.equal(conn.ev.listenerCount('messages.upsert'),0);
45
+ });
46
+ function status(conn, id='status', message={conversation:'Texto original'}) {
47
+ return api.captureStatus({key:{id,remoteJid:'status@broadcast',participant:chat},messageTimestamp:now(),message},conn);
48
+ }
49
+ test('repair status ZIP preserves text, background colors and metadata',async()=>{
50
+ const conn=socket();status(conn,'color',{extendedTextMessage:{text:'Hola',backgroundArgb:4289374890,textArgb:4294967295}});
51
+ const result=await api.exportStatusZip(conn,chat);const entries=unzipSync(result.buffer);
52
+ assert.equal(result.count,1);assert.equal(strFromU8(entries['E1.txt']),'Hola');
53
+ const info=JSON.parse(strFromU8(entries['informacion.json']));assert.equal(info.states[0].backgroundArgb,4289374890);
54
+ });
55
+ test('repair status ZIP rejects contacts not saved and synchronized',async()=>{
56
+ const conn=socket();conn.chats={};status(conn);await assert.rejects(api.exportStatusZip(conn,chat),/STATUS_CONTACT_REQUIRED/);
57
+ });
58
+ test('repair status ZIP records download failures rather than inventing content',async()=>{
59
+ const conn=socket();status(conn,'media',{imageMessage:{mimetype:'image/jpeg'}});
60
+ const result=await api.exportStatusZip(conn,chat,{downloader:async()=>{throw new Error('not delivered');}});
61
+ const info=JSON.parse(strFromU8(unzipSync(result.buffer)['informacion.json']));
62
+ assert.equal(result.count,0);assert.equal(info.omitted.length,1);assert.match(info.omitted[0].reason,/not delivered/);
63
+ });
64
+ test('repair status archive writes restricted files and sends only once',async()=>{
65
+ const conn=socket(), dir=fs.mkdtempSync(path.join(os.tmpdir(),'kage-repair-')), entry=status(conn);
66
+ const settings={statusSave:true,statusContactsOnly:true,statusDestination:'34612345678@s.whatsapp.net'};
67
+ try {
68
+ await api.handleStatusArchive(conn,entry,settings,{directory:dir});await api.handleStatusArchive(conn,entry,settings,{directory:dir});
69
+ assert.equal(conn.sent.length,2);assert.equal(conn.sent[0][1].text,'Texto original');
70
+ assert.equal(fs.statSync(entry.archive.file).mode & 0o777,0o600);
71
+ } finally {fs.rmSync(dir,{recursive:true,force:true});}
72
+ });
73
+ test('repair status handler integrates saving independently of reactions',async()=>{
74
+ const conn=socket(),dir=fs.mkdtempSync(path.join(os.tmpdir(),'kage-save-'));
75
+ try {
76
+ const message={key:{id:'save',remoteJid:'status@broadcast',participant:chat},messageTimestamp:now(),message:{conversation:'Guardado'}};
77
+ await api.handleStatus(message,conn,{statusSave:true,statusReaction:false,statusDestination:'34612345678@s.whatsapp.net',archiveOptions:{directory:dir}});
78
+ assert.equal(conn.sent[0][1].text,'Guardado');assert.ok(api.listStatuses(conn)[0].archiveDeliveredAt);
79
+ } finally {fs.rmSync(dir,{recursive:true,force:true});}
80
+ });
81
+ test('repair status saving refuses automatic delivery to groups',async()=>{
82
+ const conn=socket(),entry=status(conn);assert.equal(await api.handleStatusArchive(conn,entry,{statusSave:true,statusDestination:'123@g.us'}),null);assert.equal(conn.sent.length,0);
31
83
  });
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const axios = require('axios');
5
+ const { fetchLatestBaileysVersion, fetchLatestKageVersion } = require('../lib/Utils/generics');
6
+ const fallback = require('../lib/Defaults/wileys-version.json').version;
7
+ async function mocked(get, fn) {
8
+ const old = axios.get;
9
+ axios.get = get;
10
+ try { await fn(); } finally { axios.get = old; }
11
+ }
12
+ test('connection version is WhatsApp WEB revision, not npm library semver', async () => {
13
+ await mocked(async (url, options) => {
14
+ assert.equal(url,'https://web.whatsapp.com/sw.js');
15
+ assert.equal(options.responseType,'text');
16
+ assert.equal(options.timeout,15000);
17
+ return {data:'self.config={"client_revision":1043857761}'};
18
+ }, async () => {
19
+ assert.deepEqual(await fetchLatestBaileysVersion(),{version:[2,3000,1043857761],isLatest:true});
20
+ });
21
+ });
22
+ test('network failure retains the included WhatsApp protocol version and original error', async () => {
23
+ const error = new Error('offline test');
24
+ await mocked(async () => {throw error}, async () => {
25
+ const result = await fetchLatestBaileysVersion();
26
+ assert.deepEqual(result.version,fallback);assert.equal(result.isLatest,false);assert.equal(result.error,error);
27
+ });
28
+ });
29
+ test('malformed version response cannot pass an undefined or npm version to socket', async () => {
30
+ await mocked(async () => ({data:'<html>service unavailable</html>'}), async () => {
31
+ const result = await fetchLatestBaileysVersion();assert.deepEqual(result.version,fallback);assert.equal(result.isLatest,false);
32
+ });
33
+ });
34
+ test('package update helper requests a real npm version document', async () => {
35
+ await mocked(async url => {assert.equal(url,'https://registry.npmjs.org/kage-baileys/latest');return {data:{version:'2.0.1'}}},async()=>{
36
+ const result=await fetchLatestKageVersion();assert.deepEqual(result.version,[2,0,1]);assert.equal(result.isLatest,true);
37
+ });
38
+ });
39
+ test('package fallback is the installed library version, not a WhatsApp version', async () => {
40
+ await mocked(async()=>{throw new Error('offline')},async()=>{
41
+ const result=await fetchLatestKageVersion();assert.deepEqual(result.version,[1,1,8]);assert.equal(result.isLatest,false);
42
+ });
43
+ });