Files
whatsappNucleo/server/services/media/downloader.ts
josedario87 80d0042c7e
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
Feature: Agregar botón para crear webhook de debug automáticamente
- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection
- Detectar si ya existe un webhook apuntando al receptor de debug
- Permitir eliminar el webhook de debug
- Incluir todos los eventos disponibles al crear el webhook
- También incluye mejoras previas de manejo de media y mensajes
2025-12-02 21:21:33 -06:00

315 lines
8.3 KiB
TypeScript

/**
* Media Downloader Service
* Downloads and caches media from WhatsApp messages using Baileys
*/
import { downloadMediaMessage, type WAMessage } from '@whiskeysockets/baileys'
import { promises as fs } from 'fs'
import path from 'path'
import { query } from '../../utils/database'
import { baileysManager } from '../baileys/manager'
// Storage directory for cached media
const STORAGE_DIR = process.env.MEDIA_STORAGE_PATH || './storage/media'
// MIME type to extension mapping
const MIME_EXTENSIONS: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'video/mp4': 'mp4',
'video/3gpp': '3gp',
'video/quicktime': 'mov',
'audio/ogg': 'ogg',
'audio/mpeg': 'mp3',
'audio/mp4': 'm4a',
'audio/aac': 'aac',
'audio/opus': 'opus',
'application/pdf': 'pdf',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/vnd.ms-excel': 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'application/vnd.ms-powerpoint': 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'application/zip': 'zip',
'application/x-rar-compressed': 'rar',
'text/plain': 'txt',
}
/**
* Get file extension from MIME type
*/
function getExtensionFromMime(mimetype: string): string {
return MIME_EXTENSIONS[mimetype] || mimetype.split('/')[1] || 'bin'
}
/**
* Ensure storage directory exists
*/
async function ensureStorageDir(instanceId: string): Promise<string> {
const dir = path.join(STORAGE_DIR, instanceId)
await fs.mkdir(dir, { recursive: true })
return dir
}
/**
* Get message type from raw message
*/
function getMediaType(rawMessage: any): string | null {
const msg = rawMessage?.message
if (!msg) return null
if (msg.imageMessage) return 'image'
if (msg.videoMessage) return 'video'
if (msg.audioMessage) return 'audio'
if (msg.documentMessage) return 'document'
if (msg.stickerMessage) return 'sticker'
return null
}
/**
* Get MIME type from raw message
*/
function getMimeType(rawMessage: any, mediaType: string): string {
const msg = rawMessage?.message
if (!msg) return 'application/octet-stream'
const messageData = msg[`${mediaType}Message`]
return messageData?.mimetype || 'application/octet-stream'
}
/**
* Download media from a message and cache it locally
*/
export async function downloadAndCacheMedia(
instanceId: string,
messageId: string
): Promise<{ path: string; mimetype: string; size: number } | null> {
// Get message from database
const result = await query(
`SELECT id, raw_message, message_type, media_cached, media_local_path, media_mimetype
FROM messages
WHERE instance_id = $1 AND message_id = $2`,
[instanceId, messageId]
)
if (result.rows.length === 0) {
console.error(`[Media] Message not found: ${messageId}`)
return null
}
const message = result.rows[0]
// If already cached, return cached path
if (message.media_cached && message.media_local_path) {
try {
await fs.access(message.media_local_path)
return {
path: message.media_local_path,
mimetype: message.media_mimetype || 'application/octet-stream',
size: 0 // Could read file size if needed
}
} catch {
// File doesn't exist, re-download
console.log(`[Media] Cached file not found, re-downloading: ${messageId}`)
}
}
const rawMessage = message.raw_message
if (!rawMessage?.message) {
console.error(`[Media] No raw message content for: ${messageId}`)
return null
}
const mediaType = getMediaType(rawMessage)
if (!mediaType) {
console.error(`[Media] Message is not a media message: ${messageId}`)
return null
}
// Get socket for this instance
const socket = baileysManager.getSocket(instanceId)
if (!socket) {
console.error(`[Media] No active socket for instance: ${instanceId}`)
return null
}
try {
// Download media using Baileys
console.log(`[Media] Downloading ${mediaType} for message: ${messageId}`)
const buffer = await downloadMediaMessage(
rawMessage as WAMessage,
'buffer',
{},
{
logger: console as any,
reuploadRequest: socket.updateMediaMessage
}
)
if (!buffer || buffer.length === 0) {
console.error(`[Media] Empty buffer received for: ${messageId}`)
return null
}
// Determine file extension
const mimetype = getMimeType(rawMessage, mediaType)
const extension = getExtensionFromMime(mimetype)
// Ensure storage directory exists
const storageDir = await ensureStorageDir(instanceId)
// Save file
const filename = `${messageId}.${extension}`
const filePath = path.join(storageDir, filename)
await fs.writeFile(filePath, buffer)
// Update database with cache info
await query(
`UPDATE messages
SET media_cached = TRUE,
media_local_path = $1,
media_mimetype = $2,
media_size_bytes = $3
WHERE instance_id = $4 AND message_id = $5`,
[filePath, mimetype, buffer.length, instanceId, messageId]
)
console.log(`[Media] Cached: ${filePath} (${buffer.length} bytes)`)
return {
path: filePath,
mimetype,
size: buffer.length
}
} catch (error) {
console.error(`[Media] Error downloading media for ${messageId}:`, error)
return null
}
}
/**
* Get cached media path for a message
* Returns null if not cached
*/
export async function getCachedMediaPath(
instanceId: string,
messageId: string
): Promise<{ path: string; mimetype: string } | null> {
const result = await query(
`SELECT media_local_path, media_mimetype
FROM messages
WHERE instance_id = $1 AND message_id = $2 AND media_cached = TRUE`,
[instanceId, messageId]
)
if (result.rows.length === 0 || !result.rows[0].media_local_path) {
return null
}
// Verify file exists
try {
await fs.access(result.rows[0].media_local_path)
return {
path: result.rows[0].media_local_path,
mimetype: result.rows[0].media_mimetype || 'application/octet-stream'
}
} catch {
// File doesn't exist, clear cache flag
await query(
`UPDATE messages SET media_cached = FALSE, media_local_path = NULL
WHERE instance_id = $1 AND message_id = $2`,
[instanceId, messageId]
)
return null
}
}
/**
* Delete cached media for a message
*/
export async function deleteCachedMedia(
instanceId: string,
messageId: string
): Promise<boolean> {
const result = await query(
`SELECT media_local_path FROM messages
WHERE instance_id = $1 AND message_id = $2 AND media_cached = TRUE`,
[instanceId, messageId]
)
if (result.rows.length === 0 || !result.rows[0].media_local_path) {
return false
}
try {
await fs.unlink(result.rows[0].media_local_path)
} catch {
// File might not exist, that's ok
}
await query(
`UPDATE messages
SET media_cached = FALSE, media_local_path = NULL, media_size_bytes = NULL
WHERE instance_id = $1 AND message_id = $2`,
[instanceId, messageId]
)
return true
}
/**
* Get total size of cached media for an instance
*/
export async function getCacheStats(instanceId: string): Promise<{
count: number
totalSize: number
}> {
const result = await query(
`SELECT COUNT(*) as count, COALESCE(SUM(media_size_bytes), 0) as total_size
FROM messages
WHERE instance_id = $1 AND media_cached = TRUE`,
[instanceId]
)
return {
count: parseInt(result.rows[0].count) || 0,
totalSize: parseInt(result.rows[0].total_size) || 0
}
}
/**
* Clear all cached media for an instance
*/
export async function clearInstanceCache(instanceId: string): Promise<number> {
// Get all cached files
const result = await query(
`SELECT media_local_path FROM messages
WHERE instance_id = $1 AND media_cached = TRUE AND media_local_path IS NOT NULL`,
[instanceId]
)
let deleted = 0
for (const row of result.rows) {
try {
await fs.unlink(row.media_local_path)
deleted++
} catch {
// File might not exist
}
}
// Clear cache flags
await query(
`UPDATE messages
SET media_cached = FALSE, media_local_path = NULL, media_size_bytes = NULL
WHERE instance_id = $1`,
[instanceId]
)
return deleted
}