kage-baileys 3.0.0 → 3.0.2
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/Example/connection.cjs +4 -0
- package/lib/Socket/chats.js +2 -2
- package/lib/Socket/messages-recv.js +9 -5
- package/lib/Utils/automation-tools.js +52 -18
- package/lib/Utils/message-inspector.js +105 -0
- package/lib/Utils/status-refresh.js +37 -0
- package/lib/Utils/status-tools.js +8 -48
- package/lib/Utils/terminal-banner.js +32 -0
- package/lib/Utils/view-once-compat.js +13 -5
- package/lib/index.d.ts +8 -4
- package/lib/index.js +5 -1
- package/package.json +4 -3
- package/test/installation-repair.test.cjs +1 -1
- package/test/recepcion-reparada.test.cjs +98 -0
- package/test/revision.test.cjs +0 -27
- package/test/status-policy.test.cjs +40 -0
package/Example/connection.cjs
CHANGED
|
@@ -29,6 +29,7 @@ async function connect() {
|
|
|
29
29
|
auth ||= await useMultiFileAuthState(path.join(dataDir, 'auth'));
|
|
30
30
|
sock = makeWASocket({
|
|
31
31
|
logger,
|
|
32
|
+
kageTools: { banner: true, eventTracking: true, statusTracking: true },
|
|
32
33
|
auth: { creds: auth.state.creds, keys: makeCacheableSignalKeyStore(auth.state.keys, logger) },
|
|
33
34
|
browser: Browsers.macOS('Desktop'),
|
|
34
35
|
syncFullHistory: process.env.KAGE_FULL_HISTORY !== '0',
|
|
@@ -40,6 +41,9 @@ async function connect() {
|
|
|
40
41
|
getMessage: async key => (await store.loadMessage(key.remoteJid, key.id))?.message
|
|
41
42
|
});
|
|
42
43
|
store.bind(sock.ev);
|
|
44
|
+
sock.ev.on('kage.event', event => {
|
|
45
|
+
if (['call', 'unsupported'].includes(event.kind)) logger.info(event, 'Evento detectado');
|
|
46
|
+
});
|
|
43
47
|
sock.ev.on('creds.update', () => {
|
|
44
48
|
credsWrite = credsWrite.then(() => auth.saveCreds()).catch(error => {
|
|
45
49
|
logger.error({ error }, 'Error de escritura de credenciales; se detiene para no perder la sesión');
|
package/lib/Socket/chats.js
CHANGED
|
@@ -737,7 +737,7 @@ const makeChatsSocket = (config) => {
|
|
|
737
737
|
return jid;
|
|
738
738
|
}
|
|
739
739
|
if ((0, WABinary_1.isLidUser)(jid)) {
|
|
740
|
-
return (0, WABinary_1.
|
|
740
|
+
return (0, WABinary_1.jidNormalizedUser)(jid);
|
|
741
741
|
}
|
|
742
742
|
return jid;
|
|
743
743
|
};
|
|
@@ -770,7 +770,7 @@ const makeChatsSocket = (config) => {
|
|
|
770
770
|
}
|
|
771
771
|
return keys;
|
|
772
772
|
};
|
|
773
|
-
const getDedupState = (msg) =>
|
|
773
|
+
const getDedupState = (msg) => ['ciphertext', 'stub', 'unknown'].includes(require('../Utils/message-inspector').inspectMessage(msg).type)
|
|
774
774
|
? 'ciphertext'
|
|
775
775
|
: 'final';
|
|
776
776
|
const getPendingDedupState = (dedupState) => dedupState === 'final' ? 'processing' : dedupState;
|
|
@@ -766,7 +766,8 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
766
766
|
const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc');
|
|
767
767
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
768
768
|
if (encNode && encNode.attrs.type === 'msmsg') {
|
|
769
|
-
logger.
|
|
769
|
+
logger.warn({ id: node.attrs.id, from: node.attrs.from }, 'msmsg no compatible: contenido cifrado no procesado');
|
|
770
|
+
ev.emit('kage.unsupported-message', { id: node.attrs.id, chat: node.attrs.from, encryption: 'msmsg', reason: 'UNSUPPORTED_ENCRYPTION' });
|
|
770
771
|
await sendMessageAck(node);
|
|
771
772
|
return;
|
|
772
773
|
}
|
|
@@ -822,7 +823,9 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
822
823
|
// no type in the receipt => message delivered
|
|
823
824
|
let type = undefined;
|
|
824
825
|
if ((_b = msg.key.participant) === null || _b === void 0 ? void 0 : _b.endsWith('@lid')) {
|
|
825
|
-
msg.key.participant
|
|
826
|
+
const originalLid = msg.key.participant;
|
|
827
|
+
const mapped = node.attrs.participant_pn || await Promise.resolve(signalRepository.lidMapping?.getPNForLID?.(originalLid)).catch(() => undefined);
|
|
828
|
+
if (mapped) { msg.key.participantAlt = originalLid; msg.key.participant = mapped; }
|
|
826
829
|
}
|
|
827
830
|
if ((0, WABinary_1.isJidGroup)(msg.key.remoteJid) && ((_f = (_e = (_d = (_c = msg.message) === null || _c === void 0 ? void 0 : _c.extendedTextMessage) === null || _d === void 0 ? void 0 : _d.contextInfo) === null || _e === void 0 ? void 0 : _e.participant) === null || _f === void 0 ? void 0 : _f.endsWith('@lid'))) {
|
|
828
831
|
if (msg.message.extendedTextMessage.contextInfo) {
|
|
@@ -835,9 +838,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
835
838
|
void warmupGroupParticipants(msg.key.remoteJid);
|
|
836
839
|
}
|
|
837
840
|
if (!(0, WABinary_1.isJidGroup)(msg.key.remoteJid) && (0, WABinary_1.isLidUser)(msg.key.remoteJid)) {
|
|
838
|
-
msg.key.remoteJid
|
|
839
|
-
|
|
840
|
-
||
|
|
841
|
+
const originalLid = msg.key.remoteJid;
|
|
842
|
+
const mapped = node.attrs.sender_pn || node.attrs.peer_recipient_pn
|
|
843
|
+
|| await Promise.resolve(signalRepository.lidMapping?.getPNForLID?.(originalLid)).catch(() => undefined);
|
|
844
|
+
if (mapped) { msg.key.remoteJidAlt = originalLid; msg.key.remoteJid = mapped; }
|
|
841
845
|
}
|
|
842
846
|
let participant = msg.key.participant;
|
|
843
847
|
if (category === 'peer') { // special peer message
|
|
@@ -8,7 +8,7 @@ function unwrapNormalMedia(raw) {
|
|
|
8
8
|
let msg = raw?.message;
|
|
9
9
|
for (let i = 0; i < 12 && msg; i++) {
|
|
10
10
|
if (msg.viewOnceMessage || msg.viewOnceMessageV2 || msg.viewOnceMessageV2Extension) throw new Error('VIEW_ONCE_PROTECTED: pide al remitente una copia normal con su consentimiento.');
|
|
11
|
-
const wrapper = ['ephemeralMessage', 'documentWithCaptionMessage'].find(k => msg[k]?.message);
|
|
11
|
+
const wrapper = ['deviceSentMessage', 'ephemeralMessage', 'documentWithCaptionMessage'].find(k => msg[k]?.message);
|
|
12
12
|
if (wrapper) { msg = msg[wrapper].message; continue; }
|
|
13
13
|
for (const type of ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage']) {
|
|
14
14
|
if (!msg[type]) continue;
|
|
@@ -33,16 +33,32 @@ async function downloadNormalMedia(conn, raw, options = {}) {
|
|
|
33
33
|
const chunks = [];
|
|
34
34
|
let size = 0;
|
|
35
35
|
const job = (async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
let target = { ...raw, message: media.message };
|
|
37
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
38
|
+
chunks.length = 0; size = 0;
|
|
39
|
+
try {
|
|
40
|
+
stream = await download(target, 'stream', { options: { timeout: timeoutMs } }, { logger, reuploadRequest: conn.updateMediaMessage?.bind(conn) });
|
|
41
|
+
if (stopped) { stream.destroy?.(); throw new Error('MEDIA_TIMEOUT'); }
|
|
42
|
+
for await (const chunk of stream) {
|
|
43
|
+
if (stopped) throw new Error('MEDIA_TIMEOUT');
|
|
44
|
+
const data = Buffer.from(chunk); size += data.length;
|
|
45
|
+
if (size > maxBytes) throw new Error('MEDIA_TOO_LARGE: se interrumpió al alcanzar el límite.');
|
|
46
|
+
chunks.push(data);
|
|
47
|
+
}
|
|
48
|
+
if (!size) throw new Error('EMPTY_MEDIA: WhatsApp no entregó datos.');
|
|
49
|
+
return { buffer: Buffer.concat(chunks, size), type: media.type, mimetype: media.value.mimetype || 'application/octet-stream', caption: media.value.caption || '', bytes: size };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
stream?.destroy?.();
|
|
52
|
+
const code = error?.output?.statusCode || error?.response?.status || error?.statusCode;
|
|
53
|
+
const retry = [403,404,408,410,429,500,502,503,504].includes(Number(code)) || /EMPTY_MEDIA|ECONNRESET|ETIMEDOUT|ENOTFOUND/.test(error?.message || error?.code || '');
|
|
54
|
+
if (attempt || stopped || !retry) throw error;
|
|
55
|
+
if ([403,404,410].includes(Number(code)) && typeof conn.updateMediaMessage === 'function') {
|
|
56
|
+
const updated = await conn.updateMediaMessage(target);
|
|
57
|
+
if (updated?.message) target = updated;
|
|
58
|
+
}
|
|
59
|
+
if (stopped) throw new Error('MEDIA_TIMEOUT');
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
|
-
if (!size) throw new Error('EMPTY_MEDIA: WhatsApp no entregó datos.');
|
|
45
|
-
return { buffer: Buffer.concat(chunks, size), type: media.type, mimetype: media.value.mimetype || 'application/octet-stream', caption: media.value.caption || '', bytes: size };
|
|
46
62
|
})();
|
|
47
63
|
try {
|
|
48
64
|
return await Promise.race([job, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('MEDIA_TIMEOUT: tiempo de espera agotado.')), timeoutMs); })]);
|
|
@@ -96,7 +112,7 @@ function handleAutoMedia(raw, conn, settings = {}, options = {}) {
|
|
|
96
112
|
while (state.seen.size > 500) state.seen.delete(state.seen.keys().next().value);
|
|
97
113
|
state.pending++;
|
|
98
114
|
const job = state.tail.then(() => saveNormalMedia(conn, raw, options));
|
|
99
|
-
state.tail = job.catch(() => {}).finally(() => { state.pending--; });
|
|
115
|
+
state.tail = job.catch(() => { state.seen.delete(id); }).finally(() => { state.pending--; });
|
|
100
116
|
return job;
|
|
101
117
|
}
|
|
102
118
|
async function sendAIResponse(conn, jid, text, options = {}) {
|
|
@@ -113,8 +129,13 @@ function installKageTools(conn, options = {}) {
|
|
|
113
129
|
Object.defineProperty(conn, '__kageToolsInstalled', { value: true });
|
|
114
130
|
const status = require('./status-tools');
|
|
115
131
|
const country = require('./country-info');
|
|
132
|
+
conn.inspectMessage = raw => require('./message-inspector').inspectMessage(raw);
|
|
133
|
+
conn.getMessageDiagnostics = () => require('./message-inspector').getMessageDiagnostics(conn);
|
|
134
|
+
conn.enableEventTracking = opts => require('./message-inspector').installMessageInspector(conn, opts);
|
|
135
|
+
conn.refreshReceivedStatuses = opts => require('./status-refresh').refreshReceivedStatuses(conn, opts);
|
|
136
|
+
if (options.eventTracking) conn.enableEventTracking(options.eventOptions || {});
|
|
137
|
+
if (options.banner) require('./terminal-banner').showKageBanner();
|
|
116
138
|
conn.getReceivedStatuses = (includeUnavailable = false) => status.listStatuses(conn, includeUnavailable);
|
|
117
|
-
conn.reactToReceivedStatus = (entry, emoji) => status.reactToStatus(conn, entry, emoji);
|
|
118
139
|
conn.getCountryForJid = jid => country.getCountryForJid(conn, jid);
|
|
119
140
|
conn.downloadNormalMedia = (raw, opts) => downloadNormalMedia(conn, raw, opts);
|
|
120
141
|
conn.saveNormalMedia = (raw, opts) => saveNormalMedia(conn, raw, opts);
|
|
@@ -130,17 +151,25 @@ function installKageTools(conn, options = {}) {
|
|
|
130
151
|
conn.enableMediaAutomation = (settings = {}) => {
|
|
131
152
|
if (conn.__kageStopMedia) return conn.__kageStopMedia;
|
|
132
153
|
let active = true;
|
|
154
|
+
const recent = new Map();
|
|
133
155
|
const scoped = new Proxy(settings, { get(target, key) { return !active && ['autoDownloadChats','autoViewOnceChats','autoViewOnceConsent'].includes(key) ? undefined : target[key]; } });
|
|
134
156
|
const upsert = event => {
|
|
135
|
-
if (event?.type
|
|
157
|
+
if (event?.type && !['notify', 'append'].includes(event.type)) return;
|
|
136
158
|
for (const raw of event.messages || []) {
|
|
159
|
+
const id = JSON.stringify([raw.key?.remoteJid, raw.key?.id]);
|
|
160
|
+
if (raw.messageTimestamp) recent.set(id, raw.messageTimestamp);
|
|
161
|
+
while(recent.size > 1000) recent.delete(recent.keys().next().value);
|
|
137
162
|
handleAutoMedia(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'media automation'));
|
|
138
163
|
require('./view-once-compat').handleAutoViewOnce(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'view once automation'));
|
|
139
164
|
}
|
|
140
165
|
};
|
|
141
|
-
const
|
|
166
|
+
const updated = items => { for (const item of items || []) if (item.update?.message) {
|
|
167
|
+
const timestamp = item.update.messageTimestamp || recent.get(JSON.stringify([item.key?.remoteJid, item.key?.id]));
|
|
168
|
+
if (timestamp) upsert({ type: 'append', messages: [{ ...item.update, key: item.key, messageTimestamp: timestamp }] });
|
|
169
|
+
} };
|
|
170
|
+
const stop = () => { active = false; recent.clear(); conn.ev.off('messages.update', updated); conn.ev.off('messages.upsert', upsert); conn.ev.off('connection.update', closed); delete conn.__kageStopMedia; };
|
|
142
171
|
const closed = update => { if (update.connection === 'close') stop(); };
|
|
143
|
-
conn.ev.on('messages.upsert', upsert); conn.ev.on('connection.update', closed);
|
|
172
|
+
conn.ev.on('messages.upsert', upsert); conn.ev.on('messages.update', updated); conn.ev.on('connection.update', closed);
|
|
144
173
|
conn.__kageStopMedia = stop;
|
|
145
174
|
return stop;
|
|
146
175
|
};
|
|
@@ -150,13 +179,18 @@ function installKageTools(conn, options = {}) {
|
|
|
150
179
|
conn.enableStatusTracking = (settings = {}) => {
|
|
151
180
|
if (stop) return stop;
|
|
152
181
|
let active = true;
|
|
153
|
-
const scopedSettings = new Proxy(settings, { get(target, key) { return key === '
|
|
182
|
+
const scopedSettings = new Proxy(settings, { get(target, key) { return key === 'statusSave' ? active && target[key] : target[key]; } });
|
|
154
183
|
const upsert = event => { for (const raw of event?.messages || []) status.handleStatus(raw, conn, scopedSettings).catch(e => conn.logger?.warn?.({ error: e.message }, 'status tracking')); };
|
|
184
|
+
const history = event => upsert(event);
|
|
155
185
|
const deleted = event => { for (const key of event?.keys || []) status.markStatusDeleted(conn, key); };
|
|
156
|
-
const updated = items => { for (const item of items || [])
|
|
186
|
+
const updated = items => { for (const item of items || []) {
|
|
187
|
+
if (item.key?.remoteJid === 'status@broadcast' && item.update?.message === null) status.markStatusDeleted(conn, item.key);
|
|
188
|
+
else if (item.update?.message) upsert({ messages: [{ ...item.update, key: item.key }] });
|
|
189
|
+
} };
|
|
157
190
|
const closed = update => { if (update.connection === 'close') stop?.(); };
|
|
158
191
|
const timer = setInterval(() => status.flushStatuses(conn), 60000); timer.unref?.();
|
|
159
192
|
conn.ev.on('messages.upsert', upsert);
|
|
193
|
+
conn.ev.on('messaging-history.set', history);
|
|
160
194
|
conn.ev.on('messages.delete', deleted);
|
|
161
195
|
conn.ev.on('messages.update', updated);
|
|
162
196
|
conn.ev.on('connection.update', closed);
|
|
@@ -164,7 +198,7 @@ function installKageTools(conn, options = {}) {
|
|
|
164
198
|
if (!active) return;
|
|
165
199
|
active = false;
|
|
166
200
|
clearInterval(timer);
|
|
167
|
-
conn.ev.off('messages.upsert', upsert); conn.ev.off('messages.delete', deleted);
|
|
201
|
+
conn.ev.off('messages.upsert', upsert); conn.ev.off('messaging-history.set', history); conn.ev.off('messages.delete', deleted);
|
|
168
202
|
conn.ev.off('messages.update', updated); conn.ev.off('connection.update', closed);
|
|
169
203
|
status.flushStatuses(conn); stop = undefined;
|
|
170
204
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const states = new WeakMap();
|
|
3
|
+
const wrappers = ['deviceSentMessage', 'ephemeralMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'documentWithCaptionMessage', 'editedMessage', 'statusMentionMessage', 'groupStatusMentionMessage', 'groupStatusMessage', 'groupStatusMessageV2'];
|
|
4
|
+
const labels = { conversation:'Texto', extendedTextMessage:'Texto', imageMessage:'Imagen', videoMessage:'Video', audioMessage:'Audio', documentMessage:'Documento', stickerMessage:'Sticker', reactionMessage:'Reacción', encReactionMessage:'Reacción cifrada', contactMessage:'Contacto', contactsArrayMessage:'Contactos', locationMessage:'Ubicación', liveLocationMessage:'Ubicación en vivo', pollCreationMessage:'Encuesta', pollCreationMessageV2:'Encuesta', pollCreationMessageV3:'Encuesta', pollUpdateMessage:'Voto de encuesta', protocolMessage:'Protocolo', interactiveMessage:'Interactivo', interactiveResponseMessage:'Respuesta interactiva', buttonsResponseMessage:'Respuesta de botón', listResponseMessage:'Respuesta de lista', eventMessage:'Evento programado', call:'Llamada', scheduledCallCreationMessage:'Llamada programada' };
|
|
5
|
+
const clean = jid => String(jid || '').replace(/:\d+@/, '@');
|
|
6
|
+
const numeric = n => { const value = Number(n?.toNumber?.() ?? n ?? 0); return Number.isFinite(value) ? value : 0; };
|
|
7
|
+
function inspectMessage(raw = {}) {
|
|
8
|
+
let message = raw.message || raw.fakeObj?.message || {};
|
|
9
|
+
const layers = [];
|
|
10
|
+
for (let i=0; i<16; i++) {
|
|
11
|
+
const wrapper = wrappers.find(k => message[k]?.message);
|
|
12
|
+
if (!wrapper) break;
|
|
13
|
+
layers.push(wrapper); message = message[wrapper].message;
|
|
14
|
+
}
|
|
15
|
+
const keys = Object.keys(message).filter(k => message[k] != null && !['messageContextInfo','senderKeyDistributionMessage','fastRatchetKeySenderKeyDistributionMessage'].includes(k));
|
|
16
|
+
const type = keys.find(k => labels[k]) || keys[0] || '';
|
|
17
|
+
const value = typeof message[type] === 'object' ? message[type] : {};
|
|
18
|
+
const context = value?.contextInfo || {};
|
|
19
|
+
const key = raw.key || raw.fakeObj?.key || {};
|
|
20
|
+
const chat = clean(key.remoteJid || raw.chat);
|
|
21
|
+
const group = chat.endsWith('@g.us');
|
|
22
|
+
const stubType = numeric(raw.messageStubType);
|
|
23
|
+
const encrypted = stubType === 2 && !type;
|
|
24
|
+
const viewOnce = layers.some(k=>k.startsWith('viewOnce')) || value?.viewOnce === true;
|
|
25
|
+
const text = typeof message[type] === 'string' ? message[type] : value?.text || value?.caption || '';
|
|
26
|
+
return {
|
|
27
|
+
id: key.id || raw.id || null, chat, chatAlt: clean(key.remoteJidAlt) || null,
|
|
28
|
+
sender: clean(key.participant || raw.participant || raw.sender || (!key.fromMe && !group && chat !== 'status@broadcast' ? chat : '')) || null,
|
|
29
|
+
senderAlt: clean(key.participantAlt) || null, fromMe: !!key.fromMe,
|
|
30
|
+
route: chat === 'status@broadcast' ? 'estado' : group ? 'grupo' : chat.endsWith('@newsletter') ? 'canal' : /@(s\.whatsapp\.net|lid)$/.test(chat) ? 'privado' : 'sistema',
|
|
31
|
+
type: type || (encrypted ? 'ciphertext' : stubType ? 'stub' : 'unknown'),
|
|
32
|
+
label: encrypted ? 'Mensaje cifrado pendiente de descifrar' : labels[type] || (stubType ? 'Aviso del sistema' : type || 'Sin contenido'),
|
|
33
|
+
wrappers: layers, viewOnce, ephemeral: layers.includes('ephemeralMessage') || numeric(context.expiration)>0,
|
|
34
|
+
ptt: !!value?.ptt, gif: !!value?.gifPlayback, mime: value?.mimetype || null,
|
|
35
|
+
bytes: numeric(value?.fileLength) || (typeof text === 'string' ? Buffer.byteLength(text) : 0),
|
|
36
|
+
width: numeric(value?.width), height: numeric(value?.height), seconds: numeric(value?.seconds),
|
|
37
|
+
timestamp: numeric(raw.messageTimestamp), stubType: stubType || null,
|
|
38
|
+
quotedId: context.stanzaId || null, mentions: (context.mentionedJid || []).slice(0,100),
|
|
39
|
+
forwarded: !!context.isForwarded, forwardingScore: numeric(context.forwardingScore),
|
|
40
|
+
downloadable: !!(value?.mediaKey && (value?.directPath || value?.url)), encrypted,
|
|
41
|
+
receivedAt: Date.now()
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function stateFor(conn) {
|
|
45
|
+
let state=states.get(conn);
|
|
46
|
+
if(!state){state={events:[],messages:new Map(),counts:{},listeners:[],stopped:false};states.set(conn,state);}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
function rememberMessage(conn, raw) {
|
|
50
|
+
const info=inspectMessage(raw); const state=stateFor(conn);
|
|
51
|
+
if(info.id){
|
|
52
|
+
const key=JSON.stringify([info.chat,info.sender,info.id,info.fromMe]);
|
|
53
|
+
const previous=state.messages.get(key) || [...state.messages.values()].find(item=>item.chat===info.chat&&item.id===info.id&&item.fromMe===info.fromMe);
|
|
54
|
+
if (!info.timestamp && previous) info.timestamp=previous.timestamp;
|
|
55
|
+
if (!info.sender && previous) info.sender=previous.sender;
|
|
56
|
+
state.messages.set(key,info);
|
|
57
|
+
}
|
|
58
|
+
for(const [key,info] of state.messages) if(Date.now()-info.receivedAt>86400000) state.messages.delete(key);
|
|
59
|
+
while(state.messages.size>1000) state.messages.delete(state.messages.keys().next().value);
|
|
60
|
+
return info;
|
|
61
|
+
}
|
|
62
|
+
function installMessageInspector(conn, options={}) {
|
|
63
|
+
const state=stateFor(conn);
|
|
64
|
+
if(state.stop && !state.stopped) return state.stop;
|
|
65
|
+
state.stopped=false;
|
|
66
|
+
const record=(kind,detail)=>{
|
|
67
|
+
const event={kind,time:Date.now(),...detail};
|
|
68
|
+
state.events.push(event); while(state.events.length>100) state.events.shift();
|
|
69
|
+
state.counts[kind]=(state.counts[kind]||0)+1;
|
|
70
|
+
conn.ev.emit('kage.event',event);
|
|
71
|
+
if(typeof options.onEvent==='function') { try{options.onEvent(event);}catch(error){conn.logger?.warn?.({error:error.message},'event monitor callback');} }
|
|
72
|
+
};
|
|
73
|
+
const bind=(name,callback)=>{conn.ev.on(name,callback);state.listeners.push([name,callback]);};
|
|
74
|
+
bind('messages.upsert',data=>{for(const raw of data?.messages||[]) record('message',rememberMessage(conn,raw));});
|
|
75
|
+
bind('messaging-history.set',data=>{for(const raw of data?.messages||[]) rememberMessage(conn,raw);record('history',{count:data?.messages?.length||0});});
|
|
76
|
+
bind('messages.update',items=>{for(const item of items||[]) {
|
|
77
|
+
if(item.update?.message) record('message.updated',rememberMessage(conn,{...item.update,key:item.key}));
|
|
78
|
+
else record('message.receipt',{id:item.key?.id,chat:clean(item.key?.remoteJid),status:item.update?.status ?? null,deleted:item.update?.message===null});
|
|
79
|
+
}});
|
|
80
|
+
bind('messages.delete',data=>record('message.deleted',{keys:(data?.keys||[]).slice(0,100).map(k=>({id:k.id,chat:clean(k.remoteJid)})),all:!!data?.all}));
|
|
81
|
+
bind('messages.reaction',items=>{for(const item of items||[]) record('reaction',{id:item.key?.id,chat:clean(item.key?.remoteJid),emoji:item.reaction?.text||''});});
|
|
82
|
+
bind('message-receipt.update',items=>{for(const item of items||[]) record('receipt',{id:item.key?.id,chat:clean(item.key?.remoteJid),user:clean(item.receipt?.userJid)});});
|
|
83
|
+
bind('call',calls=>{for(const call of calls||[]) record('call',{id:call.id,chat:clean(call.chatId||call.from),from:clean(call.from),status:call.status,isVideo:!!call.isVideo,isGroup:!!call.isGroup});});
|
|
84
|
+
bind('presence.update',data=>record('presence',{chat:clean(data?.id),users:Object.entries(data?.presences||{}).slice(0,30).map(([jid,p])=>({jid:clean(jid),state:p?.lastKnownPresence}))}));
|
|
85
|
+
bind('group-participants.update',data=>record('group.participants',{chat:clean(data?.id),action:data?.action,participants:(data?.participants||[]).slice(0,100).map(p=>typeof p==='string'?p:p.id)}));
|
|
86
|
+
bind('groups.update',items=>{for(const item of items||[])record('group.updated',{chat:clean(item.id),fields:Object.keys(item).filter(k=>k!=='id')});});
|
|
87
|
+
bind('chats.upsert',items=>record('chats.upsert',{chats:(items||[]).map(item=>clean(item.id)).slice(0,100)}));
|
|
88
|
+
bind('chats.update',items=>record('chats.update',{chats:(items||[]).map(item=>clean(item.id)).slice(0,100)}));
|
|
89
|
+
bind('contacts.upsert',items=>record('contacts.upsert',{count:items?.length||0}));
|
|
90
|
+
bind('contacts.update',items=>record('contacts.update',{count:items?.length||0}));
|
|
91
|
+
bind('kage.automation',data=>record('automation.'+String(data.kind||'media'),{id:data.id,chat:clean(data.chat),outcome:data.outcome,error:data.error||null}));
|
|
92
|
+
bind('kage.unsupported-message',data=>record('unsupported',{id:data?.id,chat:clean(data?.chat),encryption:data?.encryption,reason:data?.reason}));
|
|
93
|
+
bind('connection.update',data=>{record('connection',{connection:data.connection||null,receivedPendingNotifications:data.receivedPendingNotifications??null});if(data.connection==='close')state.stop();});
|
|
94
|
+
state.stop=()=>{if(state.stopped)return;state.stopped=true;for(const [name,fn]of state.listeners)conn.ev.off(name,fn);state.listeners=[];};
|
|
95
|
+
return state.stop;
|
|
96
|
+
}
|
|
97
|
+
function getMessageDiagnostics(conn) {
|
|
98
|
+
const state=stateFor(conn);
|
|
99
|
+
// Never expose references to internal mutable buffers or credentials.
|
|
100
|
+
return JSON.parse(JSON.stringify({active:!!state.stop&&!state.stopped,counts:state.counts,recent:state.events, messages:[...state.messages.values()]}));
|
|
101
|
+
}
|
|
102
|
+
exports.inspectMessage=inspectMessage;
|
|
103
|
+
exports.rememberMessage=rememberMessage;
|
|
104
|
+
exports.installMessageInspector=installMessageInspector;
|
|
105
|
+
exports.getMessageDiagnostics=getMessageDiagnostics;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const cooldowns=new WeakMap();
|
|
3
|
+
async function refreshReceivedStatuses(conn, options={}) {
|
|
4
|
+
const status=require('./status-tools');
|
|
5
|
+
const capture=options.capture || (raw=>status.captureStatus(raw,conn));
|
|
6
|
+
const list=options.list || (()=>status.listStatuses(conn));
|
|
7
|
+
let state=cooldowns.get(conn);if(!state){state={last:0,keys:new Map(),job:null};cooldowns.set(conn,state);}
|
|
8
|
+
if(state.job)return state.job;
|
|
9
|
+
if(Date.now()-state.last<10000)return {received:list().length,imported:0,requested:0,errors:[],throttled:true};
|
|
10
|
+
state.last=Date.now();
|
|
11
|
+
state.job=(async()=>{
|
|
12
|
+
let imported=0,requested=0;const errors=[];
|
|
13
|
+
const sources=[options.messages,conn.chats?.['status@broadcast']?.messages,options.store?.messages?.['status@broadcast']];
|
|
14
|
+
if(options.capture) sources.push(status.listStatuses(conn).map(entry=>entry.raw).filter(Boolean));
|
|
15
|
+
for(const source of sources){
|
|
16
|
+
const items=Array.isArray(source)?source:source instanceof Map?[...source.values()]:source?.array||[];
|
|
17
|
+
for(const raw of items.slice(-500))if(raw?.key?.remoteJid==='status@broadcast'&&capture(raw))imported++;
|
|
18
|
+
}
|
|
19
|
+
const entries=list();
|
|
20
|
+
// There is no arbitrary status-list query here. Request only known message keys delivered to us.
|
|
21
|
+
const missing=entries.filter(entry=>!entry.type&&entry.sourceKey?.id&&!['expired','deleted'].includes(entry.availability)&&entry.expiresAt>Date.now());
|
|
22
|
+
for(const entry of missing.slice(0,5)){
|
|
23
|
+
const key=JSON.stringify([entry.author,entry.sourceKey.id]);
|
|
24
|
+
if(Date.now()-(state.keys.get(key)||0)<60000)continue;
|
|
25
|
+
if(typeof conn.requestPlaceholderResend!=='function')break;
|
|
26
|
+
state.keys.set(key,Date.now()); requested++;
|
|
27
|
+
let timer;
|
|
28
|
+
try { await Promise.race([conn.requestPlaceholderResend(entry.sourceKey),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('STATUS_REFRESH_PENDING')),4000);})]); }
|
|
29
|
+
catch(error){errors.push({id:entry.id,error:String(error.message||error).slice(0,200)});}
|
|
30
|
+
finally{clearTimeout(timer);}
|
|
31
|
+
}
|
|
32
|
+
for(const [key,time]of state.keys)if(Date.now()-time>86400000)state.keys.delete(key);
|
|
33
|
+
return {received:list().length,imported,requested,errors,throttled:false};
|
|
34
|
+
})();
|
|
35
|
+
try{return await state.job;}finally{state.job=null;}
|
|
36
|
+
}
|
|
37
|
+
exports.refreshReceivedStatuses=refreshReceivedStatuses;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Estados recibidos por ESTA conexión. No consulta estados ocultos ni audiencias privadas.
|
|
3
3
|
const { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync } = require('node:fs')
|
|
4
4
|
const { resolve, join } = require('node:path')
|
|
5
|
-
const { createHash
|
|
5
|
+
const { createHash } = require('node:crypto')
|
|
6
6
|
const reportError = (area, error) => console.error(`[${area}]`, error?.message || error)
|
|
7
7
|
const caches = new WeakMap()
|
|
8
8
|
const DAY = 86_400_000
|
|
@@ -19,8 +19,10 @@ function persist(state) {
|
|
|
19
19
|
}
|
|
20
20
|
function stateFor(conn) {
|
|
21
21
|
let state = caches.get(conn)
|
|
22
|
+
const identity = normalizeJid(conn.user?.jid || conn.user?.id)
|
|
23
|
+
if (state && state.account !== identity) { persist(state); caches.delete(conn); state = null }
|
|
22
24
|
if (!state) {
|
|
23
|
-
state = { entries: new Map(), next: 1, last: 0, busy: false }
|
|
25
|
+
state = { entries: new Map(), next: 1, last: 0, busy: false, account: identity }
|
|
24
26
|
const account = normalizeJid(conn.user?.jid || conn.user?.id)
|
|
25
27
|
if (account && conn.statusStorageDir !== false) {
|
|
26
28
|
state.dir = resolve(conn.statusStorageDir || 'data/status')
|
|
@@ -71,7 +73,7 @@ function statusContent(message = {}) {
|
|
|
71
73
|
const contexts = []
|
|
72
74
|
for (let i = 0; i < 12 && current; i++) {
|
|
73
75
|
for (const value of Object.values(current)) if (value?.contextInfo) contexts.push(value.contextInfo)
|
|
74
|
-
const key = ['ephemeralMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension',
|
|
76
|
+
const key = ['deviceSentMessage', 'ephemeralMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension',
|
|
75
77
|
'documentWithCaptionMessage', 'statusMentionMessage', 'groupStatusMentionMessage', 'groupStatusMessage', 'groupStatusMessageV2']
|
|
76
78
|
.find(name => current[name]?.message)
|
|
77
79
|
if (!key) break
|
|
@@ -155,8 +157,9 @@ function captureStatus(raw, conn) {
|
|
|
155
157
|
viewOnce: !!(old?.viewOnce || info.wrappers.some(w => /viewOnce/.test(w))),
|
|
156
158
|
routes: [...new Set([...(old?.routes || []), raw.key.remoteJid])]
|
|
157
159
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const stored = old ? Object.assign(old, entry) : entry
|
|
161
|
+
state.entries.set(identity, stored); prune(state); persist(state)
|
|
162
|
+
return stored
|
|
160
163
|
}
|
|
161
164
|
function listStatuses(conn, includeUnavailable = false) { const state = stateFor(conn); prune(state); return [...state.entries.values()].filter(e => includeUnavailable || !['deleted', 'expired'].includes(e.availability)).sort((a, b) => a.receivedAt - b.receivedAt) }
|
|
162
165
|
function findStatus(conn, selector = '') {
|
|
@@ -164,27 +167,6 @@ function findStatus(conn, selector = '') {
|
|
|
164
167
|
return listStatuses(conn, true).find(entry => entry.id === id || entry.sourceKey?.id === selector)
|
|
165
168
|
}
|
|
166
169
|
function clearStatuses(conn) { const state = stateFor(conn); const count = state.entries.size; for (const entry of state.entries.values()) retire(entry, 'expired', Date.now()); state.entries.clear(); persist(state); return count }
|
|
167
|
-
function isStatusEmoji(text) {
|
|
168
|
-
if (typeof text !== 'string' || text.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20e3/u.test(text)) return false
|
|
169
|
-
return [...new Intl.Segmenter('es', { granularity: 'grapheme' }).segment(text)].length === 1
|
|
170
|
-
}
|
|
171
|
-
async function reactToStatus(conn, entry, emoji, options = {}) {
|
|
172
|
-
if (!isStatusEmoji(emoji)) throw new Error('Escribe un solo emoji, por ejemplo ❤️, 👍🏽 o 👨👩👧👦.')
|
|
173
|
-
if (!entry || ['deleted', 'expired'].includes(entry.availability) || entry.expiresAt <= Date.now()) throw new Error('Ese estado ya no está disponible en la caché.')
|
|
174
|
-
if (!entry.sourceKey?.id || !/@(s\.whatsapp\.net|lid)$/.test(entry.author)) throw new Error('La notificación no incluye la clave y el autor originales; no es posible reaccionar con seguridad.')
|
|
175
|
-
const self = normalizeJid(conn.user?.jid || conn.user?.id)
|
|
176
|
-
if (entry.author === self || entry.author === normalizeJid(conn.user?.lid) || entry.sourceKey.fromMe) throw new Error('No se reacciona a los estados de la propia cuenta.')
|
|
177
|
-
const state = stateFor(conn)
|
|
178
|
-
if (state.busy) throw new Error('Hay otra reacción en curso; espera unos segundos.')
|
|
179
|
-
const interval = Math.max(1000, Number(options.intervalMs ?? global.safety?.statusReactionIntervalMs) || 5000)
|
|
180
|
-
if (Date.now() - state.last < interval) throw new Error('Espera unos segundos entre reacciones.')
|
|
181
|
-
state.busy = true; state.last = Date.now()
|
|
182
|
-
try {
|
|
183
|
-
if (typeof conn.readMessages === 'function') await conn.readMessages([entry.sourceKey])
|
|
184
|
-
await conn.sendMessage('status@broadcast', { react: { text: emoji, key: entry.sourceKey } }, { statusJidList: [entry.author] })
|
|
185
|
-
entry.reactedWith = emoji; entry.reactedAt = Date.now(); state.last = Date.now(); persist(state)
|
|
186
|
-
} finally { state.busy = false }
|
|
187
|
-
}
|
|
188
170
|
async function handleStatus(raw, conn, settings = {}) {
|
|
189
171
|
if (raw?.key?.remoteJid !== 'status@broadcast') return false
|
|
190
172
|
const entry = captureStatus(raw, conn)
|
|
@@ -192,27 +174,8 @@ async function handleStatus(raw, conn, settings = {}) {
|
|
|
192
174
|
try { await require('./status-archive').handleStatusArchive(conn, entry, settings, settings.archiveOptions || {}) }
|
|
193
175
|
catch (error) { reportError('status-archive', error) }
|
|
194
176
|
}
|
|
195
|
-
if (!entry || !settings.statusReaction || raw.key.fromMe || entry.reactedAt) return true
|
|
196
|
-
const state = stateFor(conn)
|
|
197
|
-
state.scheduled ||= new Set()
|
|
198
|
-
if (state.scheduled.has(entry.id) || state.scheduled.size >= 30) return true
|
|
199
|
-
state.scheduled.add(entry.id)
|
|
200
|
-
const task = (state.reactionTail || Promise.resolve()).then(async () => {
|
|
201
|
-
const gap = Math.max(1000, Number(settings.statusReactionIntervalMs ?? global.safety?.statusReactionIntervalMs) || 5000)
|
|
202
|
-
const wait = Math.max(0, state.last + gap - Date.now())
|
|
203
|
-
if (wait) await new Promise(resolve => setTimeout(resolve, wait))
|
|
204
|
-
if (!settings.statusReaction || entry.reactedAt || ['deleted', 'expired'].includes(entry.availability)) return
|
|
205
|
-
const pool = Array.isArray(settings.statusEmojis) ? settings.statusEmojis.filter(isStatusEmoji) : ['❤️', '🔥', '👍', '😊', '👏', '💜']
|
|
206
|
-
const emoji = settings.statusEmojiMode === 'random' ? pool.length ? pool[randomInt(pool.length)] : '❤️' : settings.statusEmoji || '❤️'
|
|
207
|
-
if (!isStatusEmoji(emoji)) return
|
|
208
|
-
const self = normalizeJid(conn.user?.jid || conn.user?.id)
|
|
209
|
-
if (!entry.author || entry.author === self || entry.author === normalizeJid(conn.user?.lid)) return
|
|
210
|
-
await reactToStatus(conn, entry, emoji, { intervalMs: gap })
|
|
211
|
-
})
|
|
212
|
-
state.reactionTail = task.catch(error => reportError('reacciones-estados', error)).finally(() => state.scheduled.delete(entry.id))
|
|
213
177
|
return true
|
|
214
178
|
}
|
|
215
|
-
async function waitStatusReactions(conn) { await stateFor(conn).reactionTail }
|
|
216
179
|
|
|
217
180
|
exports.markStatusDeleted = markStatusDeleted;
|
|
218
181
|
exports.flushStatuses = flushStatuses;
|
|
@@ -221,8 +184,5 @@ exports.captureStatus = captureStatus;
|
|
|
221
184
|
exports.listStatuses = listStatuses;
|
|
222
185
|
exports.findStatus = findStatus;
|
|
223
186
|
exports.clearStatuses = clearStatuses;
|
|
224
|
-
exports.isStatusEmoji = isStatusEmoji;
|
|
225
|
-
exports.reactToStatus = reactToStatus;
|
|
226
187
|
exports.handleStatus = handleStatus;
|
|
227
188
|
|
|
228
|
-
exports.waitStatusReactions = waitStatusReactions;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const chalk=require('chalk');
|
|
3
|
+
const gradient=require('gradient-string');
|
|
4
|
+
const tinyMap=Object.fromEntries([...('abcdefghijklmnopqrstuvwxyz')].map((c,i)=>[c,[...'ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ'][i]]));
|
|
5
|
+
function say(text, options={}) {
|
|
6
|
+
const stream=options.stream||process.stdout;
|
|
7
|
+
let rendered=String(text??'').replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
8
|
+
if(options.font==='tiny')rendered=[...rendered.toLowerCase()].map(c=>tinyMap[c]||c).join('');
|
|
9
|
+
else if(options.font==='block')rendered=rendered.toUpperCase().split('\n').map(line=>'▰ '+[...line].join(' ')+' ▰').join('\n');
|
|
10
|
+
else if(options.font==='simple')rendered=rendered.toUpperCase();
|
|
11
|
+
const width=Number(options.width)||stream.columns||80;
|
|
12
|
+
const color=options.forceColor??!!stream.isTTY;
|
|
13
|
+
const colors=(options.colors||[]).filter(name=>typeof chalk[name]==='function');
|
|
14
|
+
rendered=rendered.split('\n').map((line,i)=>{
|
|
15
|
+
const pad=options.align==='center'?Math.max(0,Math.floor((width-[...line].length)/2)):options.align==='right'?Math.max(0,width-[...line].length):0;
|
|
16
|
+
let output=line;
|
|
17
|
+
if(color&&Array.isArray(options.gradient)&&options.gradient.length>=2){try{output=gradient(options.gradient)(line);}catch{}}
|
|
18
|
+
else if(color&&colors.length){const paint=new chalk.Instance({level:1});output=[...line].map((char,index)=>paint[colors[(index+i)%colors.length]](char)).join('');}
|
|
19
|
+
return ' '.repeat(Math.min(500,pad))+output;
|
|
20
|
+
}).join('\n');
|
|
21
|
+
stream.write(rendered+'\n'); return rendered;
|
|
22
|
+
}
|
|
23
|
+
let shown=false;
|
|
24
|
+
function showKageBanner(options={}) {
|
|
25
|
+
if(shown||process.env.KAGE_BANNER==='0')return;
|
|
26
|
+
shown=true;
|
|
27
|
+
say('kage baileys',{font:'block',align:'center',gradient:['red','magenta'],...options});
|
|
28
|
+
say('Creado por xrljosedv',{font:'console',align:'center',colors:['cyan','magenta','yellow'],...options});
|
|
29
|
+
say('Mensajes · Estados · Llamadas',{font:'tiny',align:'center',colors:['cyan','blue'],...options});
|
|
30
|
+
}
|
|
31
|
+
exports.say=say;
|
|
32
|
+
exports.showKageBanner=showKageBanner;
|
|
@@ -7,7 +7,7 @@ function getViewOnceContent(raw) {
|
|
|
7
7
|
let message = raw?.message ?? raw;
|
|
8
8
|
let viewOnce = false;
|
|
9
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);
|
|
10
|
+
const wrapper = ['deviceSentMessage', 'ephemeralMessage', 'documentWithCaptionMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension'].find(k => message[k]?.message);
|
|
11
11
|
if (wrapper) { viewOnce ||= wrapper.startsWith('viewOnce'); message = message[wrapper].message; continue; }
|
|
12
12
|
const type = types.find(k => message[k]);
|
|
13
13
|
if (!type) return null;
|
|
@@ -40,12 +40,19 @@ async function resendViewOnceMessage(conn, raw, options = {}) {
|
|
|
40
40
|
if (field === 'document') body.fileName = value.fileName || 'archivo';
|
|
41
41
|
return conn.sendMessage(options.jid || raw.key.remoteJid, body, options.quoted ? { quoted: options.quoted } : {});
|
|
42
42
|
}
|
|
43
|
-
function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
|
|
43
|
+
async function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
|
|
44
44
|
const chat = raw?.key?.remoteJid;
|
|
45
|
-
const
|
|
45
|
+
const normalize = jid => String(jid || '').replace(/:\d+@/, '@');
|
|
46
|
+
const aliases = new Set([chat, raw?.key?.remoteJidAlt].filter(Boolean).map(normalize));
|
|
47
|
+
for (const jid of [...aliases]) {
|
|
48
|
+
if (jid.endsWith('@lid')) {
|
|
49
|
+
try { const pn = await conn.signalRepository?.lidMapping?.getPNForLID?.(jid); if (pn) aliases.add(normalize(pn)); } catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const active = () => Array.isArray(settings.autoViewOnceChats) && settings.autoViewOnceChats.some(jid => aliases.has(normalize(jid)));
|
|
46
53
|
// Preserve the explicitly consented archive API for existing library clients.
|
|
47
54
|
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);
|
|
55
|
+
if (!raw?.key?.id || (raw.key.fromMe && !settings.autoViewOnceOwn) || !/@(s\.whatsapp\.net|lid|g\.us)$/.test(chat || '') || !getViewOnceContent(raw)?.viewOnce) return Promise.resolve(null);
|
|
49
56
|
const timestamp = Number(raw.messageTimestamp?.toNumber?.() ?? raw.messageTimestamp ?? 0) * 1000;
|
|
50
57
|
if (!timestamp || Date.now() - timestamp > 300000 || timestamp > Date.now() + 60000) return Promise.resolve(null);
|
|
51
58
|
let state = queues.get(conn);
|
|
@@ -58,7 +65,8 @@ function handleAutoViewOnce(raw, conn, settings = {}, options = {}) {
|
|
|
58
65
|
state.seen.set(id, now); state.pending++;
|
|
59
66
|
while (state.seen.size > 500) state.seen.delete(state.seen.keys().next().value);
|
|
60
67
|
const task = state.tail.then(() => active() ? resendViewOnceMessage(conn, raw, { ...options, jid: chat, shouldSend: active }) : null);
|
|
61
|
-
|
|
68
|
+
task.then(result => conn.ev?.emit('kage.automation', { kind: 'viewonce', id: raw.key.id, chat, outcome: result ? 'sent' : 'cancelled' }), error => conn.ev?.emit('kage.automation', { kind: 'viewonce', id: raw.key.id, chat, outcome: 'failed', error: String(error.message || error).slice(0,200) })).catch(error => conn.logger?.warn?.({ error: error.message }, 'automation event listener'));
|
|
69
|
+
state.tail = task.catch(() => { state.seen.delete(id); }).finally(() => { state.pending--; });
|
|
62
70
|
return task;
|
|
63
71
|
}
|
|
64
72
|
exports.getViewOnceContent = getViewOnceContent;
|
package/lib/index.d.ts
CHANGED
|
@@ -193,8 +193,8 @@ export declare const extractSyncdPatches: any;
|
|
|
193
193
|
export declare const extractUrlFromText: any;
|
|
194
194
|
export declare const extractVideoThumb: any;
|
|
195
195
|
export declare const fetchLatestBaileysVersion: any;
|
|
196
|
+
export declare const fetchLatestKageVersion: any;
|
|
196
197
|
export declare const fetchLatestWaWebVersion: any;
|
|
197
|
-
export declare const fetchLatestWileysVersion: any;
|
|
198
198
|
export declare const findStatus: any;
|
|
199
199
|
export declare const flushStatuses: any;
|
|
200
200
|
export declare const generateCodeBlockContent: any;
|
|
@@ -249,6 +249,7 @@ export declare const getHistoryMsg: any;
|
|
|
249
249
|
export declare const getHttpStream: any;
|
|
250
250
|
export declare const getKeyAuthor: any;
|
|
251
251
|
export declare const getMediaKeys: any;
|
|
252
|
+
export declare const getMessageDiagnostics: any;
|
|
252
253
|
export declare const getMessageReportingToken: any;
|
|
253
254
|
export declare const getNextPreKeys: any;
|
|
254
255
|
export declare const getNextPreKeysNode: any;
|
|
@@ -272,7 +273,9 @@ export declare const hkdf: any;
|
|
|
272
273
|
export declare const hkdfInfoKey: any;
|
|
273
274
|
export declare const hmacSign: any;
|
|
274
275
|
export declare const initAuthCreds: any;
|
|
276
|
+
export declare const inspectMessage: any;
|
|
275
277
|
export declare const installKageTools: any;
|
|
278
|
+
export declare const installMessageInspector: any;
|
|
276
279
|
export declare const isJidBot: any;
|
|
277
280
|
export declare const isJidBroadcast: any;
|
|
278
281
|
export declare const isJidGroup: any;
|
|
@@ -283,7 +286,6 @@ export declare const isJidUser: any;
|
|
|
283
286
|
export declare const isKnownStatusContact: any;
|
|
284
287
|
export declare const isLidUser: any;
|
|
285
288
|
export declare const isRealMessage: any;
|
|
286
|
-
export declare const isStatusEmoji: any;
|
|
287
289
|
export declare const isWABusinessPlatform: any;
|
|
288
290
|
export declare const jidDecode: any;
|
|
289
291
|
export declare const jidEncode: any;
|
|
@@ -313,17 +315,20 @@ export declare const processContactAction: any;
|
|
|
313
315
|
export declare const processHistoryMessage: any;
|
|
314
316
|
export declare const processSyncAction: any;
|
|
315
317
|
export declare const promiseTimeout: any;
|
|
316
|
-
export declare const reactToStatus: any;
|
|
317
318
|
export declare const readAndEmitEventStream: any;
|
|
318
319
|
export declare const reduceBinaryNodeToDictionary: any;
|
|
320
|
+
export declare const refreshReceivedStatuses: any;
|
|
321
|
+
export declare const rememberMessage: any;
|
|
319
322
|
export declare const resendViewOnceMessage: any;
|
|
320
323
|
export declare const saveConsentedViewOnce: any;
|
|
321
324
|
export declare const saveNormalMedia: any;
|
|
322
325
|
export declare const saveReceivedStatus: any;
|
|
326
|
+
export declare const say: any;
|
|
323
327
|
export declare const sendAIResponse: any;
|
|
324
328
|
export declare const sha256: any;
|
|
325
329
|
export declare const shouldIncludeReportingToken: any;
|
|
326
330
|
export declare const shouldIncrementChatUnread: any;
|
|
331
|
+
export declare const showKageBanner: any;
|
|
327
332
|
export declare const signedKeyPair: any;
|
|
328
333
|
export declare const statusContent: any;
|
|
329
334
|
export declare const toBuffer: any;
|
|
@@ -344,7 +349,6 @@ export declare const uploadFile: any;
|
|
|
344
349
|
export declare const useMultiFileAuthState: any;
|
|
345
350
|
export declare const version: any;
|
|
346
351
|
export declare const vid2jpg: any;
|
|
347
|
-
export declare const waitStatusReactions: any;
|
|
348
352
|
export declare const writeRandomPadMax16: any;
|
|
349
353
|
export declare const xmppPreKey: any;
|
|
350
354
|
export declare const xmppSignedPreKey: any;
|
package/lib/index.js
CHANGED
|
@@ -95,7 +95,7 @@ for (const __p of __kageExtraModules) {
|
|
|
95
95
|
try { __kageMergeSafe(require(__p)); } catch (__e) { /* opcional */ }
|
|
96
96
|
}
|
|
97
97
|
try { __kageMergeSafe(require("./VoIP/index.js")); } catch (__e) { /* VoIP opcional */ }
|
|
98
|
-
exports.KAGE_BUILD = Object.freeze({ name: "kage-baileys", version: "
|
|
98
|
+
exports.KAGE_BUILD = Object.freeze({ name: "kage-baileys", version: "3.0.1", revision: "recepcion-automatica-1" });
|
|
99
99
|
|
|
100
100
|
__exportStar(require("./Utils/status-tools.js"), exports);
|
|
101
101
|
__exportStar(require("./Utils/country-info.js"), exports);
|
|
@@ -104,3 +104,7 @@ __exportStar(require("./Utils/automation-tools.js"), exports);
|
|
|
104
104
|
__exportStar(require("./Utils/consented-media.js"), exports);
|
|
105
105
|
__exportStar(require("./Utils/view-once-compat.js"), exports);
|
|
106
106
|
__exportStar(require("./Utils/status-archive.js"), exports);
|
|
107
|
+
|
|
108
|
+
__exportStar(require("./Utils/message-inspector.js"), exports);
|
|
109
|
+
__exportStar(require("./Utils/status-refresh.js"), exports);
|
|
110
|
+
__exportStar(require("./Utils/terminal-banner.js"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kage-baileys",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Kage Baileys una libreria de WhatsApp.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kage-baileys",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"check": "npm run lint && npm test",
|
|
40
40
|
"example": "node Example/connection.cjs",
|
|
41
41
|
"gen:types": "node scripts/generate-public-types.cjs",
|
|
42
|
-
"check:engine": "node engine-requirements.js"
|
|
42
|
+
"check:engine": "node engine-requirements.js",
|
|
43
|
+
"prepublishOnly": "npm run gen:protobuf && npm run gen:types && npm run check"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@adiwajshing/keyed-db": "^0.2.4",
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@types/node": "^20.0.0",
|
|
74
|
-
"protobufjs-cli": "2.7.1",
|
|
75
|
+
"protobufjs-cli": "^2.7.1",
|
|
75
76
|
"typescript": "^5.8.2"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
@@ -38,6 +38,6 @@ test('package update helper requests a real npm version document', async () => {
|
|
|
38
38
|
});
|
|
39
39
|
test('package fallback is the installed library version, not a WhatsApp version', async () => {
|
|
40
40
|
await mocked(async()=>{throw new Error('offline')},async()=>{
|
|
41
|
-
const result=await fetchLatestKageVersion();assert.deepEqual(result.version,
|
|
41
|
+
const result=await fetchLatestKageVersion();assert.deepEqual(result.version,require('../package.json').version.split('.').map(Number));assert.equal(result.isLatest,false);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test=require('node:test');const assert=require('node:assert/strict');
|
|
3
|
+
const {EventEmitter}=require('node:events');const {Readable}=require('node:stream');
|
|
4
|
+
const fs=require('node:fs');const os=require('node:os');const path=require('node:path');
|
|
5
|
+
const api=require('../lib');
|
|
6
|
+
const chat='51900000001@s.whatsapp.net';
|
|
7
|
+
const socket=()=>({ev:new EventEmitter(),user:{id:'51900000002:1@s.whatsapp.net'},statusStorageDir:false,sent:[],async sendMessage(jid,body){const sent={jid,body};this.sent.push(sent);return sent;}});
|
|
8
|
+
const raw=(type='imageMessage',wrapper='viewOnceMessageV2',route=chat)=>({key:{remoteJid:route,participant:route.endsWith('@g.us')?chat:undefined,id:'fresh',fromMe:false},messageTimestamp:Math.floor(Date.now()/1000),message:{[wrapper]:{message:{[type]:{viewOnce:true,mediaKey:Buffer.from('secret'),directPath:'/delivered',mimetype:type==='audioMessage'?'audio/ogg':'application/octet-stream',ptt:type==='audioMessage'}}}}});
|
|
9
|
+
const download=async()=>Readable.from([Buffer.from('delivered-content')]);
|
|
10
|
+
for(const type of ['imageMessage','videoMessage','audioMessage','documentMessage','stickerMessage'])for(const wrapper of ['viewOnceMessage','viewOnceMessageV2','viewOnceMessageV2Extension']){
|
|
11
|
+
test(`inspección conserva ${wrapper}/${type}`,()=>{
|
|
12
|
+
const message=raw(type,wrapper),before=JSON.stringify(message),detail=api.inspectMessage(message);
|
|
13
|
+
assert.equal(detail.type,type);assert.equal(detail.viewOnce,true);assert.equal(detail.downloadable,true);assert.equal(JSON.stringify(message),before);assert.equal(JSON.stringify(detail).includes('secret'),false);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
for(const route of [chat,'123456@g.us'])for(const type of ['imageMessage','videoMessage','audioMessage'])test(`automático usa misma salida manual: ${route}/${type}`,async()=>{
|
|
17
|
+
const a=socket(),b=socket(),message=raw(type,'viewOnceMessageV2Extension',route);
|
|
18
|
+
await api.resendViewOnceMessage(a,message,{downloader:download});
|
|
19
|
+
await api.handleAutoViewOnce(message,b,{autoViewOnceChats:[route]},{downloader:download});
|
|
20
|
+
assert.deepEqual(b.sent,a.sent);assert.equal(b.sent.length,1);
|
|
21
|
+
});
|
|
22
|
+
test('deviceSentMessage y efímero no ocultan la vista única',async()=>{
|
|
23
|
+
const c=socket(),message=raw();message.message={deviceSentMessage:{message:{ephemeralMessage:{message:message.message}}}};
|
|
24
|
+
await api.handleAutoViewOnce(message,c,{autoViewOnceChats:[chat]},{downloader:download});assert.equal(c.sent.length,1);assert.equal(api.inspectMessage(message).viewOnce,true);
|
|
25
|
+
});
|
|
26
|
+
test('permiso de privado PN funciona con LID confirmado',async()=>{
|
|
27
|
+
const c=socket();c.signalRepository={lidMapping:{getPNForLID:async()=>chat}};
|
|
28
|
+
await api.handleAutoViewOnce(raw('imageMessage','viewOnceMessageV2','999@lid'),c,{autoViewOnceChats:[chat]},{downloader:download});assert.equal(c.sent.length,1);assert.equal(c.sent[0].jid,'999@lid');
|
|
29
|
+
});
|
|
30
|
+
test('un LID sin mapeo no se inventa como teléfono ni obtiene permiso',async()=>{
|
|
31
|
+
const c=socket();await api.handleAutoViewOnce(raw('imageMessage','viewOnceMessageV2','999@lid'),c,{autoViewOnceChats:['999@s.whatsapp.net']},{downloader:download});assert.equal(c.sent.length,0);assert.equal(api.inspectMessage(raw('imageMessage','viewOnceMessageV2','999@lid')).chat,'999@lid');
|
|
32
|
+
});
|
|
33
|
+
test('medios propios solo se copian mediante autorización explícita',async()=>{
|
|
34
|
+
const c=socket(),message=raw();message.key.fromMe=true;
|
|
35
|
+
await api.handleAutoViewOnce(message,c,{autoViewOnceChats:[chat]},{downloader:download});assert.equal(c.sent.length,0);
|
|
36
|
+
await api.handleAutoViewOnce(message,c,{autoViewOnceChats:[chat],autoViewOnceOwn:true},{downloader:download});assert.equal(c.sent.length,1);
|
|
37
|
+
const normal={...message,key:{...message.key,id:'normal-output'},message:{imageMessage:{}}};await api.handleAutoViewOnce(normal,c,{autoViewOnceChats:[chat],autoViewOnceOwn:true},{downloader:download});assert.equal(c.sent.length,1);
|
|
38
|
+
});
|
|
39
|
+
test('fallo no deja el ID bloqueado para la siguiente entrega',async()=>{
|
|
40
|
+
const c=socket(),message=raw(),settings={autoViewOnceChats:[chat]};
|
|
41
|
+
await assert.rejects(api.handleAutoViewOnce(message,c,settings,{downloader:async()=>{throw new Error('fallo controlado');}}),/fallo controlado/);
|
|
42
|
+
await api.handleAutoViewOnce(message,c,settings,{downloader:download});assert.equal(c.sent.length,1);
|
|
43
|
+
});
|
|
44
|
+
test('descarga renueva referencia 404 y reintenta una vez',async()=>{
|
|
45
|
+
const c=socket();let downloads=0,reuploads=0;
|
|
46
|
+
c.updateMediaMessage=async message=>{reuploads++;return message;};
|
|
47
|
+
const result=await api.downloadViewOnceMessage(c,raw(),{downloader:async()=>{if(!downloads++){const e=new Error('CDN 404');e.response={status:404};throw e;}return download();}});
|
|
48
|
+
assert.equal(result.buffer.toString(),'delivered-content');assert.equal(downloads,2);assert.equal(reuploads,1);
|
|
49
|
+
});
|
|
50
|
+
test('fallo persistente se detiene tras dos intentos',async()=>{
|
|
51
|
+
let calls=0;await assert.rejects(api.downloadViewOnceMessage(socket(),raw(),{downloader:async()=>{calls++;const e=new Error('CDN 410');e.statusCode=410;throw e;}}),/CDN 410/);assert.equal(calls,2);
|
|
52
|
+
});
|
|
53
|
+
test('vista única automática no procesa historial antiguo',async()=>{
|
|
54
|
+
const c=socket(),message=raw();message.messageTimestamp=1;
|
|
55
|
+
await api.handleAutoViewOnce(message,c,{autoViewOnceChats:[chat]},{downloader:download});assert.equal(c.sent.length,0);
|
|
56
|
+
});
|
|
57
|
+
test('aviso cifrado no se clasifica como medio disponible',()=>{
|
|
58
|
+
const info=api.inspectMessage({key:{remoteJid:chat,id:'pending'},messageStubType:2});assert.equal(info.type,'ciphertext');assert.equal(info.encrypted,true);assert.match(info.label,/pendiente/);
|
|
59
|
+
});
|
|
60
|
+
test('monitor detecta llamadas, presencia, reacciones y cierra sin listeners duplicados',()=>{
|
|
61
|
+
const c=api.installKageTools(socket()),stop=c.enableEventTracking();assert.equal(c.enableEventTracking(),stop);
|
|
62
|
+
c.ev.emit('call',[{id:'call1',from:chat,status:'offer',isVideo:true}]);
|
|
63
|
+
c.ev.emit('presence.update',{id:chat,presences:{[chat]:{lastKnownPresence:'composing'}}});
|
|
64
|
+
c.ev.emit('messages.reaction',[{key:{id:'reaction',remoteJid:chat},reaction:{text:'👍'}}]);
|
|
65
|
+
const diag=c.getMessageDiagnostics();assert.equal(diag.counts.call,1);assert.equal(diag.counts.presence,1);assert.equal(diag.counts.reaction,1);assert.equal(diag.recent[0].isVideo,true);
|
|
66
|
+
stop();assert.equal(c.ev.listenerCount('call'),0);assert.equal(c.ev.listenerCount('messages.upsert'),0);assert.equal(c.getMessageDiagnostics().active,false);
|
|
67
|
+
});
|
|
68
|
+
test('diagnóstico acotado no almacena claves de medios ni texto',()=>{
|
|
69
|
+
const c=api.installKageTools(socket());const stop=c.enableEventTracking();
|
|
70
|
+
for(let i=0;i<120;i++){const m=raw();m.key.id='m'+i;c.ev.emit('messages.upsert',{messages:[m],type:'notify'});}
|
|
71
|
+
const diag=c.getMessageDiagnostics();assert.equal(diag.recent.length,100);assert.equal(diag.messages.length,120);assert.equal(JSON.stringify(diag).includes('secret'),false);stop();
|
|
72
|
+
});
|
|
73
|
+
test('estado de historial y actualización descifrada se reindexa',async()=>{
|
|
74
|
+
const c=api.installKageTools(socket());const stop=c.enableStatusTracking();
|
|
75
|
+
const key={remoteJid:'status@broadcast',participant:chat,id:'status1'};const timestamp=Math.floor(Date.now()/1000);
|
|
76
|
+
c.ev.emit('messaging-history.set',{messages:[{key,messageTimestamp:timestamp,message:{conversation:'publicado'}}]});
|
|
77
|
+
assert.equal(c.getReceivedStatuses().length,1);
|
|
78
|
+
c.ev.emit('messages.update',[{key,update:{messageTimestamp:timestamp,message:{imageMessage:{mimetype:'image/jpeg'}}}}]);
|
|
79
|
+
await new Promise(r=>setImmediate(r));assert.equal(c.getReceivedStatuses()[0].type,'imageMessage');stop();
|
|
80
|
+
});
|
|
81
|
+
test('actualización de estados usa solo claves conocidas, sin consultar estados ocultos',async()=>{
|
|
82
|
+
const c=socket();api.captureStatus({key:{remoteJid:'status@broadcast',participant:chat,id:'missing'},messageTimestamp:Math.floor(Date.now()/1000),message:{}},c);
|
|
83
|
+
const requests=[];c.requestPlaceholderResend=async key=>requests.push(key);
|
|
84
|
+
const report=await api.refreshReceivedStatuses(c);assert.equal(report.requested,1);assert.equal(requests[0].id,'missing');
|
|
85
|
+
assert.equal((await api.refreshReceivedStatuses(c)).throttled,true);
|
|
86
|
+
});
|
|
87
|
+
test('caché consultada antes del login carga su archivo al conocerse la cuenta',()=>{
|
|
88
|
+
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'status-login-'));
|
|
89
|
+
try{
|
|
90
|
+
const a=socket();a.statusStorageDir=dir;api.captureStatus({key:{remoteJid:'status@broadcast',participant:chat,id:'saved'},messageTimestamp:Math.floor(Date.now()/1000),message:{conversation:'persistido'}},a);
|
|
91
|
+
const b={statusStorageDir:dir};assert.equal(api.listStatuses(b).length,0);b.user=a.user;assert.equal(api.listStatuses(b).length,1);
|
|
92
|
+
}finally{fs.rmSync(dir,{recursive:true,force:true});}
|
|
93
|
+
});
|
|
94
|
+
test('banner admite fuentes y colores sin efectos al importar',()=>{
|
|
95
|
+
const lines=[];const stream={columns:60,isTTY:false,write:text=>lines.push(text)};
|
|
96
|
+
for(const font of ['simple','console','tiny','block'])api.say('kage baileys',{font,align:'center',gradient:['red','magenta'],stream});
|
|
97
|
+
assert.equal(lines.length,4);assert.match(lines[0],/KAGE BAILEYS/);assert.match(lines[3],/▰/);
|
|
98
|
+
});
|
package/test/revision.test.cjs
CHANGED
|
@@ -11,8 +11,6 @@ const countries = require('../lib/Utils/countries.json');
|
|
|
11
11
|
const examples = require('libphonenumber-js/examples.mobile.json');
|
|
12
12
|
const raw = (id = 'S1', author = '51987654321@s.whatsapp.net', message = { conversation: 'Hola' }) => ({ key: { remoteJid: 'status@broadcast', participant: author, id, fromMe: false }, messageTimestamp: Math.floor(Date.now()/1000), message });
|
|
13
13
|
const socket = () => ({ user: { id: '34612345678:2@s.whatsapp.net' }, statusStorageDir: false, sendMessage: async () => ({}), readMessages: async () => {}, ev: new EventEmitter() });
|
|
14
|
-
for (const emoji of ['❤️','🔥','👍','👍🏽','👨👩👧👦','😊','🇵🇪','1️⃣','💜','👏','🌈','🏳️🌈','🧑🏽💻','🫶','☀️','⭐','🐕','💯','😀','🎉']) test(`emoji accepted: ${emoji}`, () => assert.equal(api.isStatusEmoji(emoji), true));
|
|
15
|
-
for (const value of ['', 'hola', '❤️🔥', 'a❤️', '123', null, 2, ' ', '\n', '👍 👍', 'x', '🇵🇪🇲🇽', '❤️'.repeat(30)]) test(`emoji rejected: ${JSON.stringify(value)}`, () => assert.equal(api.isStatusEmoji(value), false));
|
|
16
14
|
for (const code of ['PE','MX','ES','AR','CO','CL','EC','BO','BR','US','CA','GB','FR','DE','IT','JP','KR','AU','NZ','IN','ZA','EG','MA','UY','PY','VE','CR','PA','DO','GT']) test(`offline country lookup ${code}`, () => {
|
|
17
15
|
const info = api.getCountryInfo(require('libphonenumber-js').getExampleNumber(code, examples).number);
|
|
18
16
|
assert.ok(info, code); assert.equal(info.code, code); assert.equal(info.name, countries[code].name);
|
|
@@ -100,23 +98,6 @@ test('status cache bounded to 500 entries', () => {
|
|
|
100
98
|
const c = socket(); for (let i=0;i<510;i++) api.captureStatus(raw('id-'+i),c);
|
|
101
99
|
assert.equal(api.listStatuses(c).length,500); assert.equal(api.clearStatuses(c),500);
|
|
102
100
|
});
|
|
103
|
-
test('manual reaction uses status route, real message key and emoji', async () => {
|
|
104
|
-
const c = socket(); let sent; c.sendMessage = async (...args) => { sent=args; };
|
|
105
|
-
const e = api.captureStatus(raw(),c); await api.reactToStatus(c,e,'👍🏽');
|
|
106
|
-
assert.equal(sent[0],'status@broadcast'); assert.equal(sent[1].react.text,'👍🏽'); assert.deepEqual(sent[2].statusJidList,[e.author]);
|
|
107
|
-
});
|
|
108
|
-
test('cannot react to deleted or self statuses', async () => {
|
|
109
|
-
const c = socket(); const e = api.captureStatus(raw(),c); api.markStatusDeleted(c,e.sourceKey);
|
|
110
|
-
await assert.rejects(api.reactToStatus(c,e,'🔥'), /disponible/);
|
|
111
|
-
const self = api.captureStatus(raw('self','34612345678@s.whatsapp.net'),c);
|
|
112
|
-
await assert.rejects(api.reactToStatus(c,self,'🔥'), /propia cuenta/);
|
|
113
|
-
});
|
|
114
|
-
test('random reaction uses configured emoji pool and deduplicates delivery', async () => {
|
|
115
|
-
const c = socket(); const sent=[]; c.sendMessage=async (...args) => { sent.push(args); };
|
|
116
|
-
const settings = { statusReaction:true,statusEmojiMode:'random',statusEmojis:['🔥','💜'] };
|
|
117
|
-
await api.handleStatus(raw(),c,settings); await api.handleStatus(raw(),c,settings); await api.waitStatusReactions(c);
|
|
118
|
-
assert.equal(sent.length,1); assert.ok(settings.statusEmojis.includes(sent[0][1].react.text));
|
|
119
|
-
});
|
|
120
101
|
test('socket bindings install and detach status listeners without duplicates', async () => {
|
|
121
102
|
const c = socket(); api.installKageTools(c); api.installKageTools(c);
|
|
122
103
|
const stop = c.enableStatusTracking(); assert.equal(c.enableStatusTracking(),stop); assert.equal(c.ev.listenerCount('messages.upsert'),1);
|
|
@@ -140,14 +121,6 @@ test('browser presets support documented calls and missing browser', () => {
|
|
|
140
121
|
assert.equal(api.Browsers().length,3); assert.doesNotThrow(()=>api.getPlatformId());
|
|
141
122
|
});
|
|
142
123
|
|
|
143
|
-
test('automatic reaction queue processes a burst with configured pacing', async () => {
|
|
144
|
-
const c = socket(); const sent=[];
|
|
145
|
-
c.sendMessage = async (...args) => { sent.push({ time: Date.now(), args }); };
|
|
146
|
-
const settings = { statusReaction: true, statusEmoji: '🔥', statusReactionIntervalMs: 1000 };
|
|
147
|
-
await api.handleStatus(raw('first'), c, settings); await api.handleStatus(raw('second'), c, settings);
|
|
148
|
-
await api.waitStatusReactions(c);
|
|
149
|
-
assert.equal(sent.length,2); assert.ok(sent[1].time - sent[0].time >= 990);
|
|
150
|
-
});
|
|
151
124
|
test('detaching status tracking never overwrites persisted activation setting', () => {
|
|
152
125
|
const c = socket(); api.installKageTools(c); const settings = { statusReaction: true };
|
|
153
126
|
const stop = c.enableStatusTracking(settings); stop(); stop();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const { EventEmitter } = require('node:events');
|
|
5
|
+
const api = require('../lib');
|
|
6
|
+
const raw = id => ({ key: { remoteJid: 'status@broadcast', participant: '51987654321@s.whatsapp.net', id }, messageTimestamp: Math.floor(Date.now()/1000), message: { conversation: 'estado' } });
|
|
7
|
+
const socket = () => ({ user: { id: '34612345678@s.whatsapp.net' }, statusStorageDir: false, ev: new EventEmitter(), sent: [], async sendMessage(...args) { this.sent.push(args); } });
|
|
8
|
+
test('Baileys has no application-level status reaction exports or socket method', () => {
|
|
9
|
+
for (const name of ['reactToStatus', 'waitStatusReactions', 'isStatusEmoji']) assert.equal(api[name], undefined);
|
|
10
|
+
const conn = api.installKageTools(socket());
|
|
11
|
+
assert.equal(conn.reactToReceivedStatus, undefined);
|
|
12
|
+
});
|
|
13
|
+
test('statusReaction settings never send reactions from Baileys', async () => {
|
|
14
|
+
const conn = socket();
|
|
15
|
+
await api.handleStatus(raw('A'), conn, { statusReaction: true, statusEmoji: '🔥' });
|
|
16
|
+
await api.handleStatus(raw('A'), conn, { statusReaction: true });
|
|
17
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
18
|
+
assert.equal(conn.sent.length, 0);
|
|
19
|
+
assert.equal(api.listStatuses(conn).length, 1);
|
|
20
|
+
});
|
|
21
|
+
test('optional status tracking only records received statuses', async () => {
|
|
22
|
+
const conn = api.installKageTools(socket());
|
|
23
|
+
const stop = conn.enableStatusTracking({ statusReaction: true });
|
|
24
|
+
conn.ev.emit('messages.upsert', { type: 'notify', messages: [raw('B')] });
|
|
25
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
26
|
+
assert.equal(conn.sent.length, 0);
|
|
27
|
+
assert.equal(conn.getReceivedStatuses().length, 1);
|
|
28
|
+
stop();
|
|
29
|
+
assert.equal(conn.ev.listenerCount('messages.upsert'), 0);
|
|
30
|
+
});
|
|
31
|
+
test('official status reaction schema survives encoding and decoding', () => {
|
|
32
|
+
const source = { reactionMessage: { key: { remoteJid: 'status@broadcast', participant: '51987654321@s.whatsapp.net', id: 'ORIGINAL', fromMe: false }, text: '❤️', senderTimestampMs: '1770000000000' } };
|
|
33
|
+
const message = api.proto.Message.fromObject(source);
|
|
34
|
+
assert.equal(api.proto.Message.verify(message), null);
|
|
35
|
+
const decoded = api.proto.Message.decode(api.proto.Message.encode(message).finish());
|
|
36
|
+
assert.equal(decoded.reactionMessage.text, '❤️');
|
|
37
|
+
assert.equal(decoded.reactionMessage.key.id, 'ORIGINAL');
|
|
38
|
+
assert.equal(decoded.reactionMessage.key.remoteJid, 'status@broadcast');
|
|
39
|
+
assert.equal(api.proto.MessageAssociation.AssociationType.STATUS_REACTION, 18);
|
|
40
|
+
});
|