up
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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>` : ''}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user