primer commit
This commit is contained in:
80
nucleo-bot/utils/decryptMediaContent.js
Normal file
80
nucleo-bot/utils/decryptMediaContent.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// utils/decryptMediaContent.js (ES modules)
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import mime from 'mime-types';
|
||||
import { decryptMedia } from '@open-wa/wa-automate';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
// 🔒 quita caracteres que rompen rutas
|
||||
const safe = s => (s || '').replace(/[\\/:*?"<>|]/g, '_');
|
||||
|
||||
export async function decryptMediaContent(
|
||||
mediaInfo,
|
||||
outputDir = 'media/dec',
|
||||
rawDir = 'media/raw',
|
||||
filename = null
|
||||
) {
|
||||
const { clientUrl, t, filehash, msgId, type } = mediaInfo;
|
||||
let { mimetype } = mediaInfo;
|
||||
|
||||
if (!clientUrl) {
|
||||
log('error', '❌ Sin clientUrl, no se puede bajar');
|
||||
return null;
|
||||
}
|
||||
|
||||
// deducir mimetype si falta
|
||||
if (!mimetype) {
|
||||
mimetype =
|
||||
mime.lookup(clientUrl) ||
|
||||
(type?.startsWith('image') && 'image/jpeg') ||
|
||||
(type?.startsWith('video') && 'video/mp4') ||
|
||||
'application/octet-stream';
|
||||
}
|
||||
|
||||
const ext = mime.extension(mimetype) || 'bin';
|
||||
const baseName = safe(filename || msgId || filehash?.slice(0,16) || `file_${t||Date.now()}`);
|
||||
const rawPath = path.join(rawDir , `${baseName}.enc`);
|
||||
const decPath = path.join(outputDir, `${baseName}.${ext}`);
|
||||
|
||||
if (fs.existsSync(decPath)) return decPath;
|
||||
|
||||
fs.mkdirSync(rawDir , { recursive: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
try {
|
||||
/* ───── descarga RAW (solo si no existe) ───── */
|
||||
if (!fs.existsSync(rawPath)) {
|
||||
const { data } = await axios
|
||||
.get(clientUrl, { responseType: 'arraybuffer' })
|
||||
.catch(e => {
|
||||
if (e.response?.status === 410) throw new Error('URL expirada (410)');
|
||||
throw e;
|
||||
});
|
||||
fs.writeFileSync(rawPath, Buffer.from(data));
|
||||
}
|
||||
|
||||
/* ───── inyectar RAW para que decryptMedia no lo vuelva a bajar ───── */
|
||||
const fake = {
|
||||
...mediaInfo,
|
||||
mimetype,
|
||||
mimeType: mimetype,
|
||||
_data: {
|
||||
...mediaInfo,
|
||||
mimetype,
|
||||
mimeType: mimetype,
|
||||
_raw: fs.readFileSync(rawPath)
|
||||
}
|
||||
};
|
||||
|
||||
const plain = await decryptMedia(fake);
|
||||
if (!plain?.length) throw new Error('descifrado vacío');
|
||||
|
||||
fs.writeFileSync(decPath, plain);
|
||||
log('info', `✔️ ${type || ext} → ${decPath}`);
|
||||
return decPath;
|
||||
} catch (e) {
|
||||
log('error', `❌ decryptMedia falló → ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
80
nucleo-bot/utils/processMessage.js
Normal file
80
nucleo-bot/utils/processMessage.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* Convierte el raw de open-wa a un objeto compacto.
|
||||
* Solo coloca `media` cuando hay TODO para desencriptar: mediaKey + clientUrl.
|
||||
*/
|
||||
export function processMessage(raw) {
|
||||
const m = raw?.data ?? raw; // alias corto
|
||||
|
||||
const base = {
|
||||
msgId : m.id,
|
||||
chatId : m.chatId,
|
||||
chatName : m.chat?.name ?? m.chat?.formattedTitle ?? null,
|
||||
senderId : m.sender?.id ?? m.author ?? null,
|
||||
senderName: m.sender?.name ?? m.notifyName ?? null,
|
||||
type : m.type,
|
||||
text : m.text ?? m.caption ?? '',
|
||||
fromMe : !!m.fromMe,
|
||||
timestamp : m.timestamp ?? m.t,
|
||||
date : m.timestamp
|
||||
? dayjs.unix(m.timestamp).utcOffset(-360).format('YYYY-MM-DD HH:mm:ss')
|
||||
: null,
|
||||
hasReactions: (m.reactions?.length ?? 0) > 0 || !!m.hasReaction,
|
||||
reactions : (m.reactions ?? []).map(r => r.aggregateEmoji)
|
||||
};
|
||||
|
||||
/* ---------- quoted ---------- */
|
||||
if (m.quotedMsg) {
|
||||
base.replyTo = {
|
||||
id : m.quotedMsg.id,
|
||||
text: m.quotedMsg.text ?? m.quotedMsg.body ?? '',
|
||||
from: m.quotedParticipant ?? null,
|
||||
type: m.quotedMsg.type
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- preview de enlaces ---------- */
|
||||
if (m.matchedText) {
|
||||
base.preview = {
|
||||
url : m.matchedText,
|
||||
title: m.title ?? null,
|
||||
description: m.description ?? null
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- media listo para desencriptar ---------- */
|
||||
const isMediaType = ['image','video','sticker','ptt','audio','document'].includes(m.type);
|
||||
const hasKeys = m.mediaKey || m.mediaData?.mediaKey;
|
||||
const hasUrl = m.clientUrl || m.directPath;
|
||||
|
||||
if (isMediaType && hasKeys && hasUrl) {
|
||||
base.media = {
|
||||
type : m.type,
|
||||
clientUrl: m.clientUrl ?? m.directPath,
|
||||
mimetype : m.mimetype,
|
||||
size : m.size ?? m.mediaData?.size,
|
||||
width : m.width,
|
||||
height : m.height,
|
||||
duration : Number(m.duration) || 0,
|
||||
filename : m.filename ?? null,
|
||||
caption : m.caption ?? null,
|
||||
|
||||
/* claves para decryptMediaContent */
|
||||
mediaKey : m.mediaKey ?? m.mediaData?.mediaKey,
|
||||
filehash : m.filehash ?? m.mediaData?.filehash,
|
||||
t : m.t ?? m.timestamp
|
||||
};
|
||||
|
||||
if (m.type === 'sticker') {
|
||||
Object.assign(base.media, {
|
||||
packId : m.stickerPackId ?? m.mediaData?.stickerPackId,
|
||||
pack : m.stickerPackName ?? m.mediaData?.stickerPackName,
|
||||
author : m.stickerPackPublisher ?? m.mediaData?.stickerPackPublisher,
|
||||
animated: m.isLottie || m.mediaData?.isAnimated || false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
80
nucleo-bot/utils/saveMedia.js
Normal file
80
nucleo-bot/utils/saveMedia.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// utils/saveMedia.js
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import mime from 'mime-types';
|
||||
import { decryptMedia } from '@open-wa/wa-automate';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import { config } from '../config.js';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
const BASE_DIR = '/media';
|
||||
const safe = s => (s || '').replace(/[\\/:*?"<>|]/g, '_');
|
||||
const ai = new GoogleGenAI({ apiKey: config.GEMINI_KEY });
|
||||
|
||||
function getShortId(msgId = '') {
|
||||
return msgId.replace(/^true_|^false_|@c\.us_/g, '').split('_')[0];
|
||||
}
|
||||
|
||||
export async function saveMedia(msg) {
|
||||
if (msg.type === 'chat') return null;
|
||||
if (!msg.clientUrl) {
|
||||
log('warn', `Sin clientUrl: ${msg.id}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ext = mime.extension(msg.mimetype || 'application/octet-stream') || 'bin';
|
||||
const chatId = safe(msg.chatId);
|
||||
const shortId = (`${msg.timestamp}${msg.type}`).toLocaleLowerCase();
|
||||
// const shortId = getShortId(msg.id);
|
||||
const folder = path.join(BASE_DIR, chatId, safe(msg.type));
|
||||
const filePath = path.join(folder, `${shortId}.${ext}`);
|
||||
const fileName = shortId // solo para Files API
|
||||
|
||||
// Buscar primero en Files API
|
||||
try {
|
||||
const existing = await ai.files.get({ name: fileName });
|
||||
log('info', `📂 ya existe en Files API: ${fileName}`);
|
||||
return { [msg.id]: existing };
|
||||
} catch (e) {
|
||||
if (e.message?.includes('INVALID_ARGUMENT')) {
|
||||
log('error', `files.get falló para ${fileName}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar en disco
|
||||
if (fs.existsSync(filePath)) {
|
||||
log('info', `📁 ya existe local: ${fileName}`, 'subiendo a Files API');
|
||||
const uploaded = await ai.files.upload({
|
||||
file: fileName,
|
||||
config: { mimeType: msg.mimetype || 'application/octet-stream', name: fileName },
|
||||
});
|
||||
log('info', `📤 subido a Files API: ${uploaded.name}`);
|
||||
return { [msg.id]: uploaded };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await axios.get(msg.clientUrl, { responseType: 'arraybuffer' });
|
||||
msg._data = { ...msg, _raw: raw.data };
|
||||
const buf = await decryptMedia(msg);
|
||||
|
||||
fs.mkdirSync(folder, { recursive: true });
|
||||
fs.writeFileSync(filePath, buf);
|
||||
log('info', `✅ guardado: ${filePath}`);
|
||||
|
||||
const uploaded = await ai.files.upload({
|
||||
file: filePath,
|
||||
config: { mimeType: msg.mimetype || 'application/octet-stream', name: fileName },
|
||||
});
|
||||
log('info', `📤 subido a Files API: ${uploaded.name}`);
|
||||
return { [msg.id]: uploaded };
|
||||
|
||||
} catch (err) {
|
||||
if (err.response?.status === 410) {
|
||||
log('warn', `URL expirada (410) para ${msg.id}`);
|
||||
} else {
|
||||
log('error', `Error al guardar media ${msg.id}: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user