kage-baileys 3.0.1 → 3.0.4

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.
@@ -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/WAProto/index.js CHANGED
@@ -3,6 +3,52 @@
3
3
 
4
4
  var $protobuf = require("protobufjs/minimal");
5
5
 
6
+ 'use strict';
7
+ // ============================================================================
8
+ // kage fix: decodificador base64 tolerante.
9
+ // Evita "Error: invalid encoding" cuando WhatsApp/devlogs entregan firmas,
10
+ // certificados o hashes con base64 URL-safe, saltos de linea, padding roto
11
+ // o un caracter suelto residual (longitud % 4 === 1). En ese ultimo caso se
12
+ // descarta unicamente el caracter colgante y el resto se decodifica intacto.
13
+ // Este archivo es inyectado automaticamente por scripts/generate-protobuf.cjs
14
+ // al final de WAProto/index.js tras cada regeneracion.
15
+ // ============================================================================
16
+ (function () {
17
+ var b64 = $protobuf.util.base64;
18
+ if (b64.__kageTolerant)
19
+ return;
20
+ var nativeDecode = b64.decode;
21
+ var nativeLength = b64.length;
22
+ var clean = function (string) {
23
+ var s = $String(string).replace(/[\r\n\t\s]/g, "").replace(/-/g, "+").replace(/_/g, "/");
24
+ var rem = s.length % 4;
25
+ if (rem === 2)
26
+ s += "==";
27
+ else if (rem === 3)
28
+ s += "=";
29
+ else if (rem === 1)
30
+ s = s.slice(0, -1); // caracter colgante: 6 bits sin datos, no aporta bytes
31
+ return s;
32
+ };
33
+ b64.length = function (string) {
34
+ if (typeof string !== "string")
35
+ return nativeLength(string);
36
+ return nativeLength(clean(string));
37
+ };
38
+ b64.decode = function (string, buffer, start) {
39
+ if (typeof string !== "string")
40
+ return nativeDecode(string, buffer, start);
41
+ var s = clean(string);
42
+ try {
43
+ return nativeDecode(s, buffer, start);
44
+ } catch (e) {
45
+ // ultimo recurso: elimina cualquier caracter residual no-base64
46
+ return nativeDecode(s.replace(/[^A-Za-z0-9+/=]/g, ""), buffer, start);
47
+ }
48
+ };
49
+ b64.__kageTolerant = true;
50
+ })();
51
+
6
52
  // Common aliases
7
53
  var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
8
54
  var $Object = $util.global.Object, $undefined = $util.global.undefined, $Error = $util.global.Error, $RangeError = $util.global.RangeError, $TypeError = $util.global.TypeError, $parseInt = $util.global.parseInt, $BigInt = $util.global.BigInt, $String = $util.global.String, $Number = $util.global.Number, $Array = $util.global.Array, $Boolean = $util.global.Boolean, $isFinite = $util.global.isFinite;
@@ -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.lidToJid)(jid);
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) => msg.messageStubType === WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT
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.debug({ key: node.attrs.key }, 'ignored msmsg');
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 = node.attrs.participant_pn || authState.creds.me.id;
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 = node.attrs.sender_pn
839
- || node.attrs.peer_recipient_pn
840
- || (0, WABinary_1.lidToJid)(msg.key.remoteJid);
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
- stream = await download({ ...raw, message: media.message }, 'stream', { options: { timeout: timeoutMs } }, { logger, reuploadRequest: conn.updateMediaMessage?.bind(conn) });
37
- if (stopped) { stream.destroy?.(); throw new Error('MEDIA_TIMEOUT'); }
38
- for await (const chunk of stream) {
39
- if (stopped) throw new Error('MEDIA_TIMEOUT');
40
- const data = Buffer.from(chunk); size += data.length;
41
- if (size > maxBytes) throw new Error('MEDIA_TOO_LARGE: se interrumpió al alcanzar el límite.');
42
- chunks.push(data);
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,6 +129,12 @@ 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
139
  conn.getCountryForJid = jid => country.getCountryForJid(conn, jid);
118
140
  conn.downloadNormalMedia = (raw, opts) => downloadNormalMedia(conn, raw, opts);
@@ -129,17 +151,25 @@ function installKageTools(conn, options = {}) {
129
151
  conn.enableMediaAutomation = (settings = {}) => {
130
152
  if (conn.__kageStopMedia) return conn.__kageStopMedia;
131
153
  let active = true;
154
+ const recent = new Map();
132
155
  const scoped = new Proxy(settings, { get(target, key) { return !active && ['autoDownloadChats','autoViewOnceChats','autoViewOnceConsent'].includes(key) ? undefined : target[key]; } });
133
156
  const upsert = event => {
134
- if (event?.type !== 'notify') return;
157
+ if (event?.type && !['notify', 'append'].includes(event.type)) return;
135
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);
136
162
  handleAutoMedia(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'media automation'));
137
163
  require('./view-once-compat').handleAutoViewOnce(raw, conn, scoped).catch(error => conn.logger?.warn?.({ error: error.message }, 'view once automation'));
138
164
  }
139
165
  };
140
- const stop = () => { active = false; conn.ev.off('messages.upsert', upsert); conn.ev.off('connection.update', closed); delete conn.__kageStopMedia; };
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; };
141
171
  const closed = update => { if (update.connection === 'close') stop(); };
142
- 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);
143
173
  conn.__kageStopMedia = stop;
144
174
  return stop;
145
175
  };
@@ -151,11 +181,16 @@ function installKageTools(conn, options = {}) {
151
181
  let active = true;
152
182
  const scopedSettings = new Proxy(settings, { get(target, key) { return key === 'statusSave' ? active && target[key] : target[key]; } });
153
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);
154
185
  const deleted = event => { for (const key of event?.keys || []) status.markStatusDeleted(conn, key); };
155
- const updated = items => { for (const item of items || []) if (item.key?.remoteJid === 'status@broadcast' && item.update?.message === null) status.markStatusDeleted(conn, item.key); };
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
+ } };
156
190
  const closed = update => { if (update.connection === 'close') stop?.(); };
157
191
  const timer = setInterval(() => status.flushStatuses(conn), 60000); timer.unref?.();
158
192
  conn.ev.on('messages.upsert', upsert);
193
+ conn.ev.on('messaging-history.set', history);
159
194
  conn.ev.on('messages.delete', deleted);
160
195
  conn.ev.on('messages.update', updated);
161
196
  conn.ev.on('connection.update', closed);
@@ -163,7 +198,7 @@ function installKageTools(conn, options = {}) {
163
198
  if (!active) return;
164
199
  active = false;
165
200
  clearInterval(timer);
166
- 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);
167
202
  conn.ev.off('messages.update', updated); conn.ev.off('connection.update', closed);
168
203
  status.flushStatuses(conn); stop = undefined;
169
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;
@@ -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
@@ -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 active = () => Array.isArray(settings.autoViewOnceChats) && settings.autoViewOnceChats.includes(chat);
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
- state.tail = task.catch(() => {}).finally(() => { state.pending--; });
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
@@ -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;
@@ -314,14 +317,18 @@ export declare const processSyncAction: any;
314
317
  export declare const promiseTimeout: any;
315
318
  export declare const readAndEmitEventStream: any;
316
319
  export declare const reduceBinaryNodeToDictionary: any;
320
+ export declare const refreshReceivedStatuses: any;
321
+ export declare const rememberMessage: any;
317
322
  export declare const resendViewOnceMessage: any;
318
323
  export declare const saveConsentedViewOnce: any;
319
324
  export declare const saveNormalMedia: any;
320
325
  export declare const saveReceivedStatus: any;
326
+ export declare const say: any;
321
327
  export declare const sendAIResponse: any;
322
328
  export declare const sha256: any;
323
329
  export declare const shouldIncludeReportingToken: any;
324
330
  export declare const shouldIncrementChatUnread: any;
331
+ export declare const showKageBanner: any;
325
332
  export declare const signedKeyPair: any;
326
333
  export declare const statusContent: any;
327
334
  export declare const toBuffer: 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: "3.0.1" });
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.1",
3
+ "version": "3.0.4",
4
4
  "description": "Kage Baileys una libreria de WhatsApp.",
5
5
  "keywords": [
6
6
  "kage-baileys",
@@ -57,6 +57,7 @@
57
57
  "form-data": "^4.0.0",
58
58
  "gradient-string": "^2.0.2",
59
59
  "jimp": "1.6.1",
60
+ "kage-baileys": "^3.0.2",
60
61
  "libphonenumber-js": "^1.10.20",
61
62
  "libsignal": "^6.0.0",
62
63
  "link-preview-js": "4.0.4",
@@ -11,10 +11,15 @@ pbjs.main(['-t', 'static-module', '-w', 'commonjs', '--keep-case', '-o', js, pat
11
11
  if (error) throw error;
12
12
  // Accept plain JSON int64 strings as well as Long instances in toObject.
13
13
  let source = fs.readFileSync(js, 'utf8');
14
+ // kage fix: inyecta decodificador base64 tolerante (evita "Error: invalid encoding"
15
+ // con base64 URL-safe, saltos de linea, padding roto o un caracter colgante residual
16
+ // en firmas/certificados de botMetadata.verificationMetadata.proofs).
17
+ const kageBase64Fix = fs.readFileSync(path.join(__dirname, 'kage-base64-fix.cjs'), 'utf8');
18
+ source = source.replace('var $protobuf = require("protobufjs/minimal");', 'var $protobuf = require("protobufjs/minimal");\n\n' + kageBase64Fix.trim());
14
19
  source = source.replace(/\$util\.Long\.prototype\.toString\.call\((message\.[\w]+)\)/g, 'kageLongString($1)');
15
20
  source = source.replace(/new \$util\.LongBits\((message\.[\w]+)\.low >>> 0, \1\.high >>> 0\)\.toNumber\((true)?\)/g, (_, value, unsigned) => `kageLongNumber(${value}, ${!!unsigned})`);
16
21
  source = source.replace(/\$util\.Long\.fromBits\((message\.[\w]+)\.low >>> 0, \1\.high >>> 0, (true|false)\)\.toBigInt\(\)/g, (_, value, unsigned) => `BigInt(kageLongString(${value}, ${unsigned}))`);
17
- source += `\nfunction kageLongString(value, unsigned) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') return String(value);\n const long = $util.Long.fromValue(value);\n return (unsigned === true ? long.toUnsigned() : long).toString();\n}\nfunction kageLongNumber(value, unsigned) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') return Number(value);\n const long = $util.Long.fromValue(value);\n return (unsigned ? long.toUnsigned() : long.toSigned()).toNumber();\n}\nrequire('./compat.cjs')(module.exports.proto);\n`;
22
+ source += `\nfunction kageLongString(value, unsigned) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') return String(value);\n const long = $util.Long.fromValue(value);\n return (unsigned === true ? long.toUnsigned() : long).toString();\n}\nfunction kageLongNumber(value, unsigned) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') return Number(value);\n const long = $util.Long.fromValue(value);\n return (unsigned ? long.toUnsigned() : long.toSigned()).toNumber();\n}\nrequire('./compat.cjs')(module.exports.proto);\n`; // kage-base64-fix.cjs es leído e inyectado arriba
18
23
  fs.writeFileSync(js, source);
19
24
  pbts.main(['-o', dts, js], error => {
20
25
  if (error) throw error;
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ // ============================================================================
3
+ // kage fix: decodificador base64 tolerante.
4
+ // Evita "Error: invalid encoding" cuando WhatsApp/devlogs entregan firmas,
5
+ // certificados o hashes con base64 URL-safe, saltos de linea, padding roto
6
+ // o un caracter suelto residual (longitud % 4 === 1). En ese ultimo caso se
7
+ // descarta unicamente el caracter colgante y el resto se decodifica intacto.
8
+ // Este archivo es inyectado automaticamente por scripts/generate-protobuf.cjs
9
+ // al final de WAProto/index.js tras cada regeneracion.
10
+ // ============================================================================
11
+ (function () {
12
+ var b64 = $protobuf.util.base64;
13
+ if (b64.__kageTolerant)
14
+ return;
15
+ var nativeDecode = b64.decode;
16
+ var nativeLength = b64.length;
17
+ var clean = function (string) {
18
+ var s = $String(string).replace(/[\r\n\t\s]/g, "").replace(/-/g, "+").replace(/_/g, "/");
19
+ var rem = s.length % 4;
20
+ if (rem === 2)
21
+ s += "==";
22
+ else if (rem === 3)
23
+ s += "=";
24
+ else if (rem === 1)
25
+ s = s.slice(0, -1); // caracter colgante: 6 bits sin datos, no aporta bytes
26
+ return s;
27
+ };
28
+ b64.length = function (string) {
29
+ if (typeof string !== "string")
30
+ return nativeLength(string);
31
+ return nativeLength(clean(string));
32
+ };
33
+ b64.decode = function (string, buffer, start) {
34
+ if (typeof string !== "string")
35
+ return nativeDecode(string, buffer, start);
36
+ var s = clean(string);
37
+ try {
38
+ return nativeDecode(s, buffer, start);
39
+ } catch (e) {
40
+ // ultimo recurso: elimina cualquier caracter residual no-base64
41
+ return nativeDecode(s.replace(/[^A-Za-z0-9+/=]/g, ""), buffer, start);
42
+ }
43
+ };
44
+ b64.__kageTolerant = true;
45
+ })();
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+ // Test del fix base64 tolerante: evita "Error: invalid encoding" en
3
+ // BotSignatureVerificationUseCaseProof (signature / certificateChain).
4
+ const { test } = require('node:test');
5
+ const assert = require('node:assert');
6
+ const path = require('path');
7
+ const WAProto = require(path.join(__dirname, '..', 'WAProto', 'index.js'));
8
+
9
+ const makeProof = (signature, certificateChain) => ({
10
+ messageContextInfo: {
11
+ botMetadata: {
12
+ verificationMetadata: {
13
+ proofs: [{ version: 1, useCase: 'WA_BOT_MSG', signature, certificateChain }]
14
+ }
15
+ }
16
+ }
17
+ });
18
+
19
+ test('base64 estandar con padding correcto se decodifica igual que siempre', () => {
20
+ const msg = WAProto.proto.Message.fromObject(makeProof('AAAA', ['QQ==']));
21
+ assert.strictEqual(Buffer.from(msg.messageContextInfo.botMetadata.verificationMetadata.proofs[0].certificateChain[0]).toString(), 'A');
22
+ });
23
+
24
+ test('base64 URL-safe (- y _) se acepta sin error', () => {
25
+ // 'a-b_' es URL-safe; en base64 estricto lanzaria invalid encoding por los caracteres
26
+ const msg = WAProto.proto.Message.fromObject(makeProof('AAAA', ['_-__']));
27
+ assert.ok(msg.messageContextInfo.botMetadata.verificationMetadata.proofs[0].certificateChain[0].length >= 0);
28
+ });
29
+
30
+ test('base64 sin padding se completa automaticamente', () => {
31
+ // 'QQ' (2 chars) sin '==' debe decodificar a 'A'
32
+ const msg = WAProto.proto.Message.fromObject(makeProof('AAAA', ['QQ']));
33
+ assert.strictEqual(Buffer.from(msg.messageContextInfo.botMetadata.verificationMetadata.proofs[0].certificateChain[0]).toString(), 'A');
34
+ });
35
+
36
+ test('base64 con saltos de linea no lanza invalid encoding', () => {
37
+ const msg = WAProto.proto.Message.fromObject(makeProof('AAAA', ['QQ==\n']));
38
+ assert.strictEqual(Buffer.from(msg.messageContextInfo.botMetadata.verificationMetadata.proofs[0].certificateChain[0]).toString(), 'A');
39
+ });
40
+
41
+ test('certificado con caracter colgante (len % 4 === 1) no lanza invalid encoding', () => {
42
+ // Reproduce el caso real de WhatsApp: cadena de 1169 chars (resto 1).
43
+ // El caracter colgante se descarta (6 bits sin datos) y el resto se decodifica.
44
+ const broken = 'TklYRUwuTWVzc2FnZUJ1aWxkZXJWNC43LVZlcmlmaWNhdGlvblNpZ25hdHVyZS5NZXRhZGF0YQ' + 'A';
45
+ const msg = WAProto.proto.Message.fromObject(makeProof('AAAA', [broken]));
46
+ const bytes = msg.messageContextInfo.botMetadata.verificationMetadata.proofs[0].certificateChain[0];
47
+ assert.ok(bytes.length > 0);
48
+ assert.strictEqual(Buffer.from(bytes).toString('utf8', 0, 8), 'NIXEL.Me');
49
+ });
@@ -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
+ });