kage-baileys 2.0.0 → 2.0.1

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
@@ -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 kage-baileys_version_json_1 = require("../Defaults/kage-baileys-version.json");
50
50
  const Types_1 = require("../Types");
51
51
  const WABinary_1 = require("../WABinary");
52
52
  const COMPANION_PLATFORM_MAP = {
@@ -259,9 +259,9 @@ 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', {
265
265
  ...options,
266
266
  responseType: 'json'
267
267
  });
@@ -273,17 +273,17 @@ const fetchLatestWileysVersion = async (options = {}) => {
273
273
  };
274
274
  } catch (error) {
275
275
  return {
276
- version: wileys_version_json_1.version,
276
+ version: kage-baileys_version_json_1.version,
277
277
  isLatest: false,
278
278
  error
279
279
  };
280
280
  }
281
281
  };
282
- exports.fetchLatestWileysVersion = fetchLatestWileysVersion;
282
+ exports.fetchLatestKageVersion = fetchLatestKageVersion;
283
283
 
284
284
  const fetchLatestBaileysVersion = async (options = {}) => {
285
285
  try {
286
- const { data } = await axios_1.default.get('https://registry.npmjs.org/wileys', {
286
+ const { data } = await axios_1.default.get('https://registry.npmjs.org/kage-baileys', {
287
287
  ...options,
288
288
  responseType: 'json'
289
289
  });
@@ -295,7 +295,7 @@ const fetchLatestBaileysVersion = async (options = {}) => {
295
295
  };
296
296
  } catch (error) {
297
297
  return {
298
- version: wileys_version_json_1.version,
298
+ version: kage-baileys_version_json_1.version,
299
299
  isLatest: false,
300
300
  error
301
301
  };
@@ -317,7 +317,7 @@ const fetchLatestWaWebVersion = async (options) => {
317
317
  const match = data.match(regex);
318
318
  if (!(match === null || match === void 0 ? void 0 : match[1])) {
319
319
  return {
320
- version: wileys_version_json_1.version,
320
+ version: kage-baileys_version_json_1.version,
321
321
  isLatest: false,
322
322
  error: {
323
323
  message: 'Could not find client revision in the fetched content'
@@ -332,7 +332,7 @@ const fetchLatestWaWebVersion = async (options) => {
332
332
  }
333
333
  catch (error) {
334
334
  return {
335
- version: wileys_version_json_1.version,
335
+ version: kage-baileys_version_json_1.version,
336
336
  isLatest: false,
337
337
  error
338
338
  };
@@ -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": "2.0.1",
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",
@@ -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
  });