This commit is contained in:
Lizandro Guarnizo
2026-02-20 11:21:06 -05:00
parent c9ec58979e
commit 602e6d3724
11 changed files with 130 additions and 30 deletions
+2 -2
View File
@@ -77,7 +77,7 @@ try {
// Formatear mensajes
$conversations = array_map(function($msg) {
// Build external URL if available (media id -> api/get_media proxy)
$external = (isset($msg['media_url']) && $msg['media_url']) ? (preg_match('#^https?://#i', $msg['media_url']) ? $msg['media_url'] : ("api/get_media.php?id=" . urlencode($msg['media_url']))) : null;
$external = (isset($msg['media_url']) && $msg['media_url']) ? (preg_match('#^https?://#i', $msg['media_url']) ? $msg['media_url'] : ("api/version/media-url.php?id=" . urlencode($msg['media_url']))) : null;
// Fallback: if no external URL but content is JSON with link, extract it
$contentStr = $msg['content'] ?? '';
@@ -99,7 +99,7 @@ try {
foreach ($candidates as $cand) {
if ($cand) {
// if looks like id or url
$external = preg_match('#^https?://#i', $cand) ? $cand : (strpos($cand, 'http') === false ? "api/get_media.php?id=" . urlencode($cand) : $cand);
$external = preg_match('#^https?://#i', $cand) ? $cand : (strpos($cand, 'http') === false ? "api/version/media-url.php?id=" . urlencode($cand) : $cand);
break;
}
}
+1
View File
@@ -140,6 +140,7 @@ if ($mediaId) {
// ── Si no hay archivo local, descargar con MediaService y guardar como cache permanente ──
if ($mediaId) {
try {
set_time_limit(180); // Dar tiempo suficiente para fotos grandes (hasta 5MB)
require_once __DIR__ . '/../../classes/MediaService.php';
$ms = new MediaService();
$subdir = date('Y/m');
+1
View File
@@ -226,6 +226,7 @@ class WhatsAppWebhook {
$localThumb = null;
if (!empty($mediaUrl) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
try {
set_time_limit(180); // Tiempo extra para descargar media pesado
$mediaService = new MediaService();
$subdir = date('Y/m'); // Organizar por año/mes
+9 -2
View File
@@ -77,7 +77,14 @@ function renderMediaMessage(msg) {
thumbUrl = `/api/version/media-url.php?url=${encodeURIComponent(external)}`;
}
} else {
thumbUrl = external;
// Route through media-url.php proxy (handles caching + auth)
if (external.indexOf('api/get_media.php?id=') !== -1 || external.indexOf('api/version/media-url.php?id=') !== -1) {
const parts = external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
thumbUrl = mid ? `/api/version/media-url.php?id=${encodeURIComponent(mid)}` : external;
} else {
thumbUrl = ensureProxyUrl(external) || external;
}
}
} else if (isValidMediaUrl(msg.media_url)) {
thumbUrl = msg.media_url;
@@ -90,7 +97,7 @@ function renderMediaMessage(msg) {
fullUrl = localFile.startsWith('/') ? localFile : `/${localFile}`;
} else if (isValidMediaUrl(mediaExternal)) {
let mid = '';
if (mediaExternal.indexOf('api/get_media.php?id=') !== -1) {
if (mediaExternal.indexOf('api/get_media.php?id=') !== -1 || mediaExternal.indexOf('api/version/media-url.php?id=') !== -1) {
const parts = mediaExternal.split('id='); mid = parts[1] ? decodeURIComponent(parts[1]) : '';
} else {
const m = mediaExternal.match(/[?&]mid=([^&]+)/i); if (m && m[1]) mid = decodeURIComponent(m[1]);
+3 -1
View File
@@ -116,8 +116,10 @@ class MediaService {
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_BUFFERSIZE => 65536,
]);
$content = curl_exec($ch);
$err = curl_error($ch);
+24 -3
View File
@@ -5704,9 +5704,9 @@ if (!isset($_SESSION['user_id'])) {
console.log('🔄 Usando media_url_external (URL absoluta):', full);
} else if (mediaUrl && /^\d+$/.test(mediaUrl)) {
// 📱 PRIORIDAD 4: Es un ID de WhatsApp (solo números) - usar URL absoluta
full = `${baseUrl}/api/get_media.php?id=${encodeURIComponent(mediaUrl)}`;
full = `${baseUrl}/api/version/media-url.php?id=${encodeURIComponent(mediaUrl)}`;
thumb = full;
console.log('📱 Usando whatsapp_media_id con get_media.php:', mediaUrl);
console.log('📱 Usando whatsapp_media_id con media-url.php:', mediaUrl);
} else if (mediaUrl && /^https?:\/\//i.test(mediaUrl)) {
// 🌐 PRIORIDAD 5: URL externa directa (puede estar caducada)
full = mediaUrl;
@@ -5719,12 +5719,33 @@ if (!isset($_SESSION['user_id'])) {
thumb = '';
}
// Si no hay URL de media, mostrar placeholder con botón de descarga
if (!full && mediaType !== 'text') {
const retryUrl = mediaUrl && /^\d+$/.test(mediaUrl)
? `${baseUrl}/api/version/media-url.php?id=${encodeURIComponent(mediaUrl)}`
: (mediaUrlExternal ? `${baseUrl}/${mediaUrlExternal.replace(/^\//, '')}` : '');
const retryBtn = retryUrl
? `<a href="${escapeHtml(retryUrl)}" target="_blank" class="btn btn-sm btn-outline-success mt-1" onclick="event.stopPropagation(); setTimeout(()=>location.reload(), 3000);"><i class="fas fa-download"></i> Descargar ahora</a>`
: '';
const iconMap = {image: 'fa-image', video: 'fa-video', audio: 'fa-microphone', document: 'fa-file', sticker: 'fa-sticky-note'};
const icon = iconMap[mediaType] || 'fa-file';
return `
<div class="message-media" style="background:rgba(0,0,0,0.04); border-radius:8px; padding:12px; text-align:center; min-width:180px;">
<i class="fas ${icon}" style="font-size:28px; color:#999; margin-bottom:6px;"></i>
<div style="font-size:12px; color:#888;">Descargando ${escapeHtml(mediaType)}...</div>
<div style="font-size:11px; color:#aaa; margin-top:2px;">En cola de descarga</div>
${retryBtn}
</div>
${caption ? `<div>${escapeHtml(caption)}</div>` : ''}
`;
}
switch (mediaType) {
case 'image':
return `
<div class="message-media">
<a href="#" data-full="${escapeHtml(full)}" data-type="image" data-caption="${escapeHtml(caption)}" onclick="openMediaLightbox(this.dataset.full, this.dataset.type, this.dataset.caption); return false;" title="Abrir imagen">
<img src="${escapeHtml(thumb)}" alt="Imagen" style="cursor:zoom-in">
<img src="${escapeHtml(thumb)}" alt="Imagen" style="cursor:zoom-in" onerror="this.parentElement.parentElement.innerHTML='<div style=\\'padding:12px;text-align:center;background:rgba(0,0,0,0.04);border-radius:8px;\\'><i class=\\'fas fa-image\\' style=\\'font-size:28px;color:#ccc;\\'></i><div style=\\'font-size:12px;color:#999;margin-top:4px;\\'>Imagen no disponible</div><a href=\\'${escapeHtml(full)}\\' target=\\'_blank\\' class=\\'btn btn-sm btn-outline-primary mt-1\\'><i class=\\'fas fa-download\\'></i> Reintentar</a></div>'">
</a>
</div>
${caption ? `<div>${escapeHtml(caption)}</div>` : ''}
+2 -2
View File
@@ -78,8 +78,8 @@ services:
- ERROR_REPORTING=0
# PHP optimizado para producción
- PHP_MEMORY_LIMIT=256M
- PHP_MAX_EXECUTION_TIME=60
- PHP_MEMORY_LIMIT=512M
- PHP_MAX_EXECUTION_TIME=300
- PHP_OPCACHE_ENABLE=1
depends_on:
+3
View File
@@ -121,6 +121,9 @@ cat > /etc/crontabs/www <<EOF
# Procesar mensajes delayed cada minuto
* * * * * cd /var/www/html && /usr/local/bin/php -r "require 'vendor/autoload.php'; use WhatsApp\Queue\RedisQueue; \$q = new RedisQueue(); \$q->processDelayed('messages'); \$q->processDelayed('media');" 2>&1 | logger -t delayed-processor
# Procesar cola de media pendiente cada 2 minutos
*/2 * * * * cd /var/www/html && /usr/local/bin/php scripts/media_worker.php 2>&1 | logger -t media-worker
# Limpiar logs antiguos (diario a las 3 AM)
0 3 * * * cd /var/www/html && /usr/local/bin/php -r "require 'classes/LoggerFactory.php'; echo LoggerFactory::cleanup(30) . ' logs eliminados';" 2>&1 | logger -t log-cleanup
+1 -1
View File
@@ -46,7 +46,7 @@ server {
index index.php index.html;
# Tamaño máximo de upload
client_max_body_size 50M;
client_max_body_size 100M;
# Logs
access_log /var/log/nginx/ssl_access.log;
@@ -0,0 +1,4 @@
-- Add attempts column and expand status enum for media_queue retry logic
ALTER TABLE media_queue
ADD COLUMN IF NOT EXISTS attempts INT NOT NULL DEFAULT 0,
MODIFY COLUMN status ENUM('pending','processing','done','failed','permanently_failed') DEFAULT 'pending';
+80 -19
View File
@@ -1,37 +1,98 @@
<?php
/**
* Worker simple para reintentar descargas de media fallidas (opcional)
* Revisa la tabla media_queue (si existe) e intenta procesar
* Worker para reintentar descargas de media fallidas
* Se ejecuta via cron cada 2 minutos
* Revisa la tabla media_queue e intenta procesar pendientes
*/
set_time_limit(300);
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../classes/MediaService.php';
$db = Database::getInstance();
$rows = $db->fetchAll('SELECT * FROM media_queue WHERE status = ? ORDER BY created_at ASC LIMIT 10', ['pending']);
// Incluir 'failed' con menos de 5 intentos para reintento
$rows = $db->fetchAll(
"SELECT * FROM media_queue WHERE (status = 'pending' OR (status = 'failed' AND (attempts IS NULL OR attempts < 5))) ORDER BY created_at ASC LIMIT 10"
);
if (empty($rows)) {
echo date('Y-m-d H:i:s') . " - No pending media jobs\n";
exit(0);
}
$ms = new MediaService();
$processed = 0;
$success = 0;
$failed = 0;
foreach ($rows as $r) {
$processed++;
$attempts = intval($r['attempts'] ?? 0) + 1;
try {
// Marcar como procesando
$db->query("UPDATE media_queue SET status = 'processing', attempts = :att WHERE id = :id", ['att' => $attempts, 'id' => $r['id']]);
$res = null;
if (!empty($r['media_id'])) {
$res = $ms->fetchAndStoreFromGraph($r['media_id'], $r['subdir'] ?? date('Y/m'));
} elseif (!empty($r['media_url'])) {
$res = $ms->fetchAndStoreFromUrl($r['media_url'], $r['subdir'] ?? date('Y/m'));
}
if ($res) {
// Actualizar fila media_queue
$db->update('media_queue', ['status' => 'done', 'result' => json_encode($res)], 'id = :id', ['id' => $r['id']]);
// Actualizar conversations si tenemos conversation_id
if (!empty($r['conversation_id'])) {
$update = [];
if (!empty($res['local_file'])) $update['local_file'] = $res['local_file'];
if (!empty($res['local_thumb'])) $update['local_thumb'] = $res['local_thumb'];
if (!empty($update)) {
$db->update('conversations', $update, 'id = :id', ['id' => $r['conversation_id']]);
}
if (preg_match('/^\d+$/', $r['media_url'])) {
$res = $ms->fetchAndStoreFromGraph($r['media_url'], $r['subdir'] ?? date('Y/m'));
} else {
$res = $ms->fetchAndStoreFromUrl($r['media_url'], $r['subdir'] ?? date('Y/m'));
}
}
if ($res && !empty($res['local_file'])) {
// Éxito
$db->query(
"UPDATE media_queue SET status = 'done', result = :res WHERE id = :id",
['res' => json_encode($res), 'id' => $r['id']]
);
// Actualizar conversations con el archivo local
if (!empty($r['conversation_id'])) {
$update = ['local_file' => $res['local_file']];
if (!empty($res['local_thumb'])) $update['local_thumb'] = $res['local_thumb'];
if (!empty($res['mime_type'])) $update['mime_type'] = $res['mime_type'];
$db->update('conversations', $update, 'id = :id', ['id' => $r['conversation_id']]);
echo date('Y-m-d H:i:s') . " - OK Media #{$r['id']} -> {$res['local_file']} (conv:{$r['conversation_id']})\n";
} else {
// Sin conversation_id: buscar por media_id/media_url
$mediaRef = !empty($r['media_id']) ? $r['media_id'] : $r['media_url'];
if ($mediaRef) {
try {
$db->query(
"UPDATE conversations SET local_file = :lf, local_thumb = :lt, mime_type = :mt WHERE (media_url = :mid OR whatsapp_media_id = :mid2) AND (local_file IS NULL OR local_file = '') LIMIT 5",
[
'lf' => $res['local_file'],
'lt' => $res['local_thumb'] ?? null,
'mt' => $res['mime_type'] ?? null,
'mid' => $mediaRef,
'mid2' => $mediaRef
]
);
} catch (Exception $e) {
error_log("[media_worker] Error updating conversations: " . $e->getMessage());
}
}
echo date('Y-m-d H:i:s') . " - OK Media #{$r['id']} -> {$res['local_file']} (sin conv_id)\n";
}
$success++;
} else {
throw new Exception('Download returned empty result');
}
} catch (Exception $e) {
$db->update('media_queue', ['status' => 'failed', 'result' => json_encode(['error' => $e->getMessage()])], 'id = :id', ['id' => $r['id']]);
$failed++;
$status = ($attempts >= 5) ? 'permanently_failed' : 'failed';
$db->query(
"UPDATE media_queue SET status = :st, attempts = :att, result = :res WHERE id = :id",
['st' => $status, 'att' => $attempts, 'res' => json_encode(['error' => $e->getMessage()]), 'id' => $r['id']]
);
echo date('Y-m-d H:i:s') . " - FAIL Media #{$r['id']} attempt {$attempts}: " . $e->getMessage() . "\n";
}
}
echo "Processed " . count($rows) . " jobs\n";
echo date('Y-m-d H:i:s') . " - Processed {$processed}: {$success} ok, {$failed} failed\n";