primer commit

This commit is contained in:
2025-05-02 13:09:42 -06:00
commit 42793abc7a
21 changed files with 5872 additions and 0 deletions

View 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;
}
}

View 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;
}

View 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;
}
}