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 +1 -1
- package/lib/Store/make-in-memory-store.js +1 -1
- package/lib/Utils/automation-tools.js +25 -0
- package/lib/Utils/consented-media.js +3 -24
- package/lib/Utils/event-buffer.js +2 -2
- package/lib/Utils/generics.js +10 -10
- package/lib/Utils/index.js +1 -1
- package/lib/Utils/{wileys-event-stream.js → kage-event-stream.js} +1 -1
- package/lib/Utils/status-archive.js +124 -0
- package/lib/Utils/status-tools.js +4 -0
- package/lib/Utils/use-multi-file-auth-state.js +1 -1
- package/lib/Utils/view-once-compat.js +68 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +3 -1
- package/package.json +2 -4
- package/test/archive-repair.test.cjs +77 -25
package/WAProto/p.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
kage
|
|
@@ -65,7 +65,7 @@ exports.default = (config = {}) => {
|
|
|
65
65
|
return Object.keys(contacts);
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
|
-
* binds to a
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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]);
|
package/lib/Utils/generics.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
262
|
+
const fetchLatestKageVersion = async (options = {}) => {
|
|
263
263
|
try {
|
|
264
|
-
const { data } = await axios_1.default.get('https://registry.npmjs.org/
|
|
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:
|
|
276
|
+
version: kage-baileys_version_json_1.version,
|
|
277
277
|
isLatest: false,
|
|
278
278
|
error
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
281
|
};
|
|
282
|
-
exports.
|
|
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/
|
|
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:
|
|
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:
|
|
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:
|
|
335
|
+
version: kage-baileys_version_json_1.version,
|
|
336
336
|
isLatest: false,
|
|
337
337
|
error
|
|
338
338
|
};
|
package/lib/Utils/index.js
CHANGED
|
@@ -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("./
|
|
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
|
|
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/
|
|
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),
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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('
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
});
|