From 83fcabe36f564d49f3bc9b29920bda1c60accf8f Mon Sep 17 00:00:00 2001 From: lizandrogd <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:48:57 -0500 Subject: [PATCH] full --- api/send_media_message.php | 108 +++++++ api/upload_media.php | 136 +++++++++ chat_window.php | 557 ++++++++++++++++++++++++++++++++++- conversations.php | 334 ++++++++++++++++++++- create_media_table.sql | 16 + execute_media_table.php | 62 ++++ services/WhatsAppService.php | 131 ++++++++ verify_media_table.php | 30 ++ 8 files changed, 1370 insertions(+), 4 deletions(-) create mode 100644 api/send_media_message.php create mode 100644 api/upload_media.php create mode 100644 create_media_table.sql create mode 100644 execute_media_table.php create mode 100644 verify_media_table.php diff --git a/api/send_media_message.php b/api/send_media_message.php new file mode 100644 index 0000000..2439c85 --- /dev/null +++ b/api/send_media_message.php @@ -0,0 +1,108 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +try { + $input = json_decode(file_get_contents('php://input'), true); + + if (!$input) { + throw new Exception('Datos no válidos'); + } + + // Validar campos requeridos + if (empty($input['recipient']) || empty($input['media_url']) || empty($input['media_type'])) { + throw new Exception('Faltan campos requeridos: recipient, media_url, media_type'); + } + + $recipient = $input['recipient']; + $mediaUrl = $input['media_url']; + $mediaType = $input['media_type']; // image, video, audio, document + $caption = $input['caption'] ?? null; + $filename = $input['filename'] ?? null; + + // Inicializar servicio de WhatsApp + $whatsapp = new WhatsAppService(); + + // Enviar según tipo de media + $response = null; + + switch ($mediaType) { + case 'image': + $response = $whatsapp->sendImageMessage($recipient, $mediaUrl, $caption); + break; + + case 'video': + $response = $whatsapp->sendVideoMessage($recipient, $mediaUrl, $caption); + break; + + case 'audio': + $response = $whatsapp->sendAudioMessage($recipient, $mediaUrl); + break; + + case 'document': + $response = $whatsapp->sendDocumentMessage($recipient, $mediaUrl, $filename, $caption); + break; + + default: + throw new Exception('Tipo de media no soportado: ' . $mediaType); + } + + if ($response && isset($response['messages'][0]['id'])) { + // Guardar en base de datos + $db = Database::getInstance(); + + // Obtener usuario + $user = $db->fetch( + "SELECT id FROM users WHERE phone_number = ?", + [$recipient] + ); + + if ($user) { + $db->insert('conversations', [ + 'user_id' => $user['id'], + 'message_id' => $response['messages'][0]['id'], + 'direction' => 'outgoing', + 'message_type' => $mediaType, + 'content' => $caption ?? $filename ?? '', + 'media_url' => $mediaUrl, + 'status' => 'sent', + 'created_at' => date('Y-m-d H:i:s') + ]); + } + + echo json_encode([ + 'success' => true, + 'message' => 'Mensaje multimedia enviado correctamente', + 'data' => $response + ]); + } else { + throw new Exception('Error al enviar mensaje: ' . json_encode($response)); + } + +} catch (Exception $e) { + error_log("Error en send_media_message.php: " . $e->getMessage()); + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} +?> diff --git a/api/upload_media.php b/api/upload_media.php new file mode 100644 index 0000000..512b87f --- /dev/null +++ b/api/upload_media.php @@ -0,0 +1,136 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +try { + // Verificar que se subió un archivo + if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + throw new Exception('No se recibió ningún archivo o hubo un error en la carga'); + } + + $file = $_FILES['file']; + $fileSize = $file['size']; + $fileName = basename($file['name']); + $fileTmpPath = $file['tmp_name']; + $fileType = $file['type']; + + // Validar tamaño según tipo + $maxSize = 16 * 1024 * 1024; // 16MB por defecto + + if (strpos($fileType, 'image/') === 0) { + $maxSize = 5 * 1024 * 1024; // 5MB para imágenes + } elseif (strpos($fileType, 'application/') === 0) { + $maxSize = 100 * 1024 * 1024; // 100MB para documentos + } + + if ($fileSize > $maxSize) { + throw new Exception('El archivo es demasiado grande. Máximo: ' . ($maxSize / 1024 / 1024) . 'MB'); + } + + // Validar tipos de archivo permitidos + $allowedTypes = [ + // Imágenes + 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', + // Videos + 'video/mp4', 'video/3gpp', 'video/quicktime', + // Audios + 'audio/aac', 'audio/mp3', 'audio/mpeg', 'audio/ogg', 'audio/amr', 'audio/webm', + // Documentos + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + ]; + + if (!in_array($fileType, $allowedTypes)) { + throw new Exception('Tipo de archivo no permitido: ' . $fileType); + } + + // Crear directorio de uploads si no existe + $uploadDir = __DIR__ . '/../uploads/'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + // Generar nombre único + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + $uniqueName = uniqid('media_', true) . '.' . $extension; + $uploadPath = $uploadDir . $uniqueName; + + // Mover archivo + if (!move_uploaded_file($fileTmpPath, $uploadPath)) { + throw new Exception('Error al guardar el archivo en el servidor'); + } + + // Construir URL pública + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST']; + $publicUrl = $protocol . '://' . $host . '/whatsapp/uploads/' . $uniqueName; + + // Determinar tipo de medio + $mediaType = 'document'; + if (strpos($fileType, 'image/') === 0) { + $mediaType = 'image'; + } elseif (strpos($fileType, 'video/') === 0) { + $mediaType = 'video'; + } elseif (strpos($fileType, 'audio/') === 0) { + $mediaType = 'audio'; + } + + // Guardar información en base de datos (opcional) + $db = Database::getInstance(); + $mediaId = $db->insert('media_files', [ + 'filename' => $fileName, + 'unique_name' => $uniqueName, + 'file_type' => $fileType, + 'file_size' => $fileSize, + 'media_type' => $mediaType, + 'file_path' => $uploadPath, + 'public_url' => $publicUrl, + 'uploaded_by' => $_SESSION['user']['id'] ?? null, + 'created_at' => date('Y-m-d H:i:s') + ]); + + echo json_encode([ + 'success' => true, + 'message' => 'Archivo subido correctamente', + 'data' => [ + 'id' => $mediaId, + 'filename' => $fileName, + 'url' => $publicUrl, + 'type' => $mediaType, + 'mime_type' => $fileType, + 'size' => $fileSize + ] + ]); + +} catch (Exception $e) { + error_log("Error en upload_media.php: " . $e->getMessage()); + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} +?> diff --git a/chat_window.php b/chat_window.php index 846ee7b..08ba0c9 100644 --- a/chat_window.php +++ b/chat_window.php @@ -145,6 +145,151 @@ if (empty($user_id)) { font-size: 0.8rem; opacity: 0.8; } + + /* Estilos para multimedia */ + .attach-btn { + background: transparent; + border: none; + color: #666; + padding: 12px 15px; + cursor: pointer; + font-size: 1.2rem; + } + + .attach-btn:hover { + color: #25D366; + } + + .media-preview { + padding: 10px; + background: #f0f0f0; + border-radius: 8px; + margin-bottom: 10px; + display: none; + } + + .media-preview.show { + display: block; + animation: slideDown 0.3s ease-out; + } + + @keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .media-preview-content { + display: flex; + align-items: center; + gap: 10px; + } + + .media-thumbnail { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 5px; + } + + .media-info { + flex: 1; + } + + .media-filename { + font-weight: 500; + font-size: 0.9rem; + color: #333; + } + + .media-filesize { + font-size: 0.8rem; + color: #666; + } + + .message-media { + margin-top: 8px; + } + + .message-media img, + .message-media video { + max-width: 100%; + border-radius: 8px; + margin-top: 5px; + } + + .message-media audio { + width: 100%; + margin-top: 5px; + } + + .message-document { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + background: #f0f0f0; + border-radius: 8px; + margin-top: 5px; + text-decoration: none; + color: inherit; + } + + .message-document:hover { + background: #e0e0e0; + } + + .message-document i { + font-size: 1.5rem; + color: #666; + } + + .mic-btn { + background: transparent; + border: none; + color: #666; + padding: 12px 15px; + cursor: pointer; + font-size: 1.2rem; + } + + .mic-btn:hover { + color: #25D366; + } + + .mic-btn.recording { + color: #dc3545; + animation: pulse 1s infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + .recording-indicator { + display: none; + padding: 10px; + background: #fff3cd; + border-radius: 8px; + margin-bottom: 10px; + align-items: center; + gap: 10px; + } + + .recording-indicator.show { + display: flex; + } + + .recording-time { + font-weight: 500; + color: #dc3545; + } @@ -206,7 +351,44 @@ if (empty($user_id)) { + +
+ + Grabando audio... + 0:00 + + +
+ + +
+
+ +
+
+
+
+ +
+ +
+
+ + + @@ -339,13 +521,44 @@ if (empty($user_id)) { messages.forEach(msg => { const isOutgoing = msg.direction === 'outgoing'; const messageClass = isOutgoing ? 'message-outgoing' : 'message-incoming'; - const messageContent = escapeHtml(msg.content) || 'Sin contenido'; + + // Verificar si es multimedia + let contentHtml = ''; + if (msg.media_url) { + // Mensaje multimedia + switch (msg.media_type) { + case 'image': + contentHtml = `Imagen`; + break; + case 'video': + contentHtml = ``; + break; + case 'audio': + contentHtml = ``; + break; + case 'document': + const filename = msg.filename || 'Documento'; + contentHtml = ` + + ${filename} + `; + break; + } + + if (msg.caption) { + contentHtml += `
${escapeHtml(msg.caption)}
`; + } + } else { + // Mensaje de texto normal + contentHtml = escapeHtml(msg.content) || 'Sin contenido'; + } + const messageTime = escapeHtml(msg.time || ''); html += `
-
${messageContent}
+
${contentHtml}
${messageTime} ${isOutgoing ? '' : ''}
@@ -377,6 +590,12 @@ if (empty($user_id)) { // Enviar mensaje async function sendMessage() { + // Si hay un archivo seleccionado, enviar multimedia + if (selectedFile) { + await sendMediaMessage(); + return; + } + const messageType = document.getElementById('messageType').value; const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); @@ -568,10 +787,344 @@ if (empty($user_id)) { showAlert(message, 'danger'); } + // ===== FUNCIONES MULTIMEDIA ===== + + let selectedFile = null; + let mediaRecorder = null; + let audioChunks = []; + let recordingStartTime = null; + let recordingInterval = null; + + // ===== GRABACIÓN DE AUDIO ===== + + // Iniciar grabación de audio + async function startRecording() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + + mediaRecorder = new MediaRecorder(stream); + audioChunks = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunks.push(event.data); + } + }; + + mediaRecorder.onstop = async () => { + const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); + + // Convertir a File object + const audioFile = new File([audioBlob], `audio_${Date.now()}.webm`, { + type: 'audio/webm' + }); + + selectedFile = audioFile; + showMediaPreview(audioFile); + + // Detener el stream + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(); + recordingStartTime = Date.now(); + + // Mostrar indicador de grabación + document.getElementById('recordingIndicator').classList.add('show'); + document.getElementById('micBtn').classList.add('recording'); + + // Actualizar tiempo de grabación + updateRecordingTime(); + recordingInterval = setInterval(updateRecordingTime, 1000); + + } catch (error) { + console.error('Error al acceder al micrófono:', error); + showAlert('No se pudo acceder al micrófono. Verifica los permisos.', 'danger'); + } + } + + // Detener grabación + function stopRecording() { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + clearInterval(recordingInterval); + document.getElementById('recordingIndicator').classList.remove('show'); + document.getElementById('micBtn').classList.remove('recording'); + } + } + + // Cancelar grabación + function cancelRecording() { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + audioChunks = []; + selectedFile = null; + clearInterval(recordingInterval); + document.getElementById('recordingIndicator').classList.remove('show'); + document.getElementById('micBtn').classList.remove('recording'); + } + } + + // Actualizar tiempo de grabación + function updateRecordingTime() { + if (!recordingStartTime) return; + + const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000); + const minutes = Math.floor(elapsed / 60); + const seconds = elapsed % 60; + + document.getElementById('recordingTime').textContent = + `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + + // ===== FUNCIONES DE ARCHIVOS ===== + + // Manejar selección de archivo + function handleFileSelect(event) { + const file = event.target.files[0]; + if (!file) return; + + // Validar tipo de archivo + const allowedTypes = { + image: ['image/jpeg', 'image/png', 'image/jpg'], + video: ['video/mp4', 'video/3gpp'], + audio: ['audio/mpeg', 'audio/mp3', 'audio/aac', 'audio/ogg'], + document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] + }; + + let mediaType = null; + for (const [type, mimes] of Object.entries(allowedTypes)) { + if (mimes.includes(file.type)) { + mediaType = type; + break; + } + } + + if (!mediaType) { + showAlert('Tipo de archivo no soportado', 'danger'); + event.target.value = ''; + return; + } + + // Validar tamaño + const maxSizes = { + image: 5 * 1024 * 1024, // 5MB + video: 16 * 1024 * 1024, // 16MB + audio: 16 * 1024 * 1024, // 16MB + document: 100 * 1024 * 1024 // 100MB + }; + + if (file.size > maxSizes[mediaType]) { + showAlert(`Archivo muy grande. Máximo: ${formatFileSize(maxSizes[mediaType])}`, 'danger'); + event.target.value = ''; + return; + } + + selectedFile = file; + showMediaPreview(file); + } + + // Mostrar preview del archivo + function showMediaPreview(file) { + const preview = document.getElementById('mediaPreview'); + const thumbnail = document.getElementById('mediaThumbnail'); + const filename = document.getElementById('mediaFilename'); + const filesize = document.getElementById('mediaFilesize'); + + filename.textContent = file.name; + filesize.textContent = formatFileSize(file.size); + + // Mostrar thumbnail si es imagen + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = function(e) { + thumbnail.src = e.target.result; + thumbnail.style.display = 'block'; + }; + reader.readAsDataURL(file); + } else if (file.type.startsWith('audio/')) { + // Mostrar reproductor para audio + const reader = new FileReader(); + reader.onload = function(e) { + thumbnail.outerHTML = ``; + }; + reader.readAsDataURL(file); + } else { + thumbnail.style.display = 'none'; + } + + preview.classList.add('show'); + } + + // Formatear tamaño de archivo + function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } + + // Cancelar subida de multimedia + function cancelMediaUpload() { + selectedFile = null; + document.getElementById('fileInput').value = ''; + + // Restaurar thumbnail si se cambió por audio + const thumbnailElement = document.getElementById('mediaThumbnail'); + if (thumbnailElement && thumbnailElement.tagName === 'AUDIO') { + thumbnailElement.outerHTML = ''; + } + + document.getElementById('mediaPreview').classList.remove('show'); + document.getElementById('mediaCaption').value = ''; + } + + // Enviar mensaje multimedia + async function sendMediaMessage() { + if (!selectedFile) return; + + try { + showTyping(); + + // Subir archivo + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('uploaded_by', userId); + + const uploadResponse = await fetch('api/upload_media.php', { + method: 'POST', + body: formData + }); + + const uploadData = await uploadResponse.json(); + + if (!uploadData.success) { + throw new Error(uploadData.error || 'Error subiendo archivo'); + } + + // Enviar mensaje con el archivo + const caption = document.getElementById('mediaCaption').value.trim(); + + const sendResponse = await whatsappManager.apiCall('send_media_message.php', { + body: { + recipient: currentUser.phone_number, + media_url: uploadData.public_url, + media_type: uploadData.media_type, + caption: caption || null, + filename: uploadData.filename + } + }); + + hideTyping(); + + if (sendResponse && sendResponse.success) { + cancelMediaUpload(); + showAlert('Multimedia enviado correctamente', 'success'); + await loadUserData(); // Recargar mensajes + } else { + throw new Error(sendResponse.error || 'Error enviando multimedia'); + } + + } catch (error) { + hideTyping(); + console.error('Error enviando multimedia:', error); + showAlert('Error enviando multimedia: ' + error.message, 'danger'); + } + } + + // Modificar renderMessage para soportar multimedia + const originalAddMessageToView = addMessageToView; + addMessageToView = function(content, type, timestamp = null) { + if (typeof content === 'object' && content.media_url) { + // Es un mensaje multimedia + return renderMediaMessage(content, type, timestamp); + } + return originalAddMessageToView(content, type, timestamp); + }; + + // Renderizar mensaje multimedia + function renderMediaMessage(message, type, timestamp = null) { + const chatMessages = document.getElementById('chatMessages'); + const messageDiv = document.createElement('div'); + messageDiv.className = `message-bubble message-${type}`; + + let mediaHtml = ''; + const mediaUrl = message.media_url || message.content; + + switch (message.media_type) { + case 'image': + mediaHtml = `Imagen`; + break; + case 'video': + mediaHtml = ``; + break; + case 'audio': + mediaHtml = ``; + break; + case 'document': + const filename = message.filename || 'Documento'; + mediaHtml = ` + + ${filename} + `; + break; + } + + let html = `
${mediaHtml}
`; + + if (message.caption) { + html += `
${escapeHtml(message.caption)}
`; + } + + if (timestamp) { + html += `
${formatTimestamp(timestamp)}
`; + } + + messageDiv.innerHTML = html; + chatMessages.appendChild(messageDiv); + scrollToBottom(); + } + + // Escape HTML + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Formatear timestamp + function formatTimestamp(timestamp) { + if (!timestamp) return ''; + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + } + // Inicializar cuando la página se carga document.addEventListener('DOMContentLoaded', function() { loadUserData(); + // Event listeners para multimedia + document.getElementById('attachBtn').addEventListener('click', function() { + document.getElementById('fileInput').click(); + }); + + document.getElementById('fileInput').addEventListener('change', handleFileSelect); + + // Event listener para micrófono + document.getElementById('micBtn').addEventListener('click', function() { + if (mediaRecorder && mediaRecorder.state === 'recording') { + stopRecording(); + } else { + startRecording(); + } + }); + // Auto-refresh cada 30 segundos para nuevos mensajes setInterval(refreshChat, 30000); }); diff --git a/conversations.php b/conversations.php index 4df09b5..7112de0 100644 --- a/conversations.php +++ b/conversations.php @@ -223,7 +223,7 @@ gap: 10px; } - .chat-input input { + .chat-input input[type="text"] { flex: 1; padding: 10px 15px; border: 1px solid #ddd; @@ -251,6 +251,92 @@ .chat-input .btn:hover { background: var(--whatsapp-green-dark); } + + #attach-btn { + background: #075E54; + } + + #attach-btn:hover { + background: #128C7E; + } + + /* Estilos para mensajes multimedia */ + .message-media { + max-width: 300px; + border-radius: 8px; + overflow: hidden; + margin-bottom: 5px; + } + + .message-media img, + .message-media video { + width: 100%; + display: block; + cursor: pointer; + } + + .message-media audio { + width: 100%; + } + + .message-document { + background: rgba(0,0,0,0.05); + padding: 10px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + } + + .message-document:hover { + background: rgba(0,0,0,0.1); + } + + .message-document i { + font-size: 24px; + color: var(--whatsapp-green); + } + + .document-info { + flex: 1; + } + + .document-name { + font-weight: 500; + font-size: 14px; + } + + .document-size { + font-size: 12px; + color: #666; + } + + #media-preview { + animation: slideUp 0.3s ease; + } + + @keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + .upload-progress { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--whatsapp-green); + transform-origin: left; + transition: transform 0.3s; + } .no-conversation { display: flex; @@ -369,7 +455,28 @@
+ + +
+ +