From 87b89b52a5cce0226132fd6e41ee5a031935f55a Mon Sep 17 00:00:00 2001 From: lizandrogd <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:02:17 -0500 Subject: [PATCH] up --- api/get_user_messages.php | 9 +- api/send_message.php | 12 ++- api/send_reaction.php | 32 ++++++ api/send_reply.php | 33 +++++++ api/webhook.php | 8 +- assets/js/app.js | 32 ++++++ config/config_enhanced.php | 60 ++++++------ conversations.php | 10 +- scripts/alter_conversations_status_safe.php | 32 ++++++ scripts/api_send_reaction_test.php | 15 +++ scripts/api_send_reply_test.php | 5 + scripts/wa_send_tests.php | 21 ++++ scripts/webhook_post_test.php | 102 ++++++++++++++++++++ services/WhatsAppService.php | 52 ++++++++++ 14 files changed, 387 insertions(+), 36 deletions(-) create mode 100644 api/send_reaction.php create mode 100644 api/send_reply.php create mode 100644 scripts/alter_conversations_status_safe.php create mode 100644 scripts/api_send_reaction_test.php create mode 100644 scripts/api_send_reply_test.php create mode 100644 scripts/wa_send_tests.php create mode 100644 scripts/webhook_post_test.php diff --git a/api/get_user_messages.php b/api/get_user_messages.php index 43d8ecb..83f41b2 100644 --- a/api/get_user_messages.php +++ b/api/get_user_messages.php @@ -31,11 +31,15 @@ try { id, user_id, COALESCE(content, message_text) as content, + message_id, + c.media_url, + u.phone_number as user_phone, direction, message_type, status, created_at - FROM conversations + FROM conversations c + LEFT JOIN users u ON c.user_id = u.id WHERE user_id = :user_id UNION ALL SELECT @@ -76,6 +80,9 @@ try { 'id' => intval($msg['id']), 'user_id' => intval($msg['user_id']), 'content' => $msg['content'] ?? '', + 'message_id' => $msg['message_id'] ?? null, + 'media_url' => $msg['media_url'] ?? null, + 'user_phone' => $msg['user_phone'] ?? null, 'direction' => $msg['direction'] ?? 'incoming', 'message_type' => $msg['message_type'] ?? 'text', 'status' => $msg['status'] ?? 'sent', diff --git a/api/send_message.php b/api/send_message.php index 5464b6d..c66b1c7 100644 --- a/api/send_message.php +++ b/api/send_message.php @@ -155,10 +155,16 @@ try { error_log("=== DEBUG WHATSAPP TEXT MESSAGE ==="); error_log("Recipient: " . $recipient); error_log("Message: " . $input['message']); - error_log("Method: sendTextMessage()"); + error_log("Method: sendTextMessage()/sendTextReply()"); error_log("===================================="); - - $response = $whatsappService->sendTextMessage($recipient, $input['message']); + + // Si viene reply_to, usar reply con contexto + if (!empty($input['reply_to'])) { + $replyTo = $input['reply_to']; + $response = $whatsappService->sendTextReply($recipient, $replyTo, $input['message']); + } else { + $response = $whatsappService->sendTextMessage($recipient, $input['message']); + } break; case 'template': diff --git a/api/send_reaction.php b/api/send_reaction.php new file mode 100644 index 0000000..8e6c51e --- /dev/null +++ b/api/send_reaction.php @@ -0,0 +1,32 @@ + false, 'error' => 'Parámetros faltantes: to, message_id, emoji']); + exit; +} + +try { + $wa = new WhatsAppService(); + $result = $wa->sendReaction($to, $message_id, $emoji, $dry); + echo json_encode(['success' => true, 'dry_run' => $dry, 'result' => $result]); +} catch (Throwable $t) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $t->getMessage()]); +} diff --git a/api/send_reply.php b/api/send_reply.php new file mode 100644 index 0000000..c614795 --- /dev/null +++ b/api/send_reply.php @@ -0,0 +1,33 @@ + false, 'error' => 'Parámetros faltantes: to, message_id, text']); + exit; +} + +try { + $wa = new WhatsAppService(); + $result = $wa->sendTextReply($to, $message_id, $text, $preview, $dry); + echo json_encode(['success' => true, 'dry_run' => $dry, 'result' => $result]); +} catch (Throwable $t) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $t->getMessage()]); +} diff --git a/api/webhook.php b/api/webhook.php index c639a6d..2108c8c 100644 --- a/api/webhook.php +++ b/api/webhook.php @@ -128,7 +128,13 @@ class WhatsAppWebhook { $messageType = 'text'; $mediaUrl = null; - if (isset($message['text'])) { + if (isset($message['reaction'])) { + // Reacción (emoji) a un mensaje anterior + $messageType = 'reaction'; + $emoji = $message['reaction']['emoji'] ?? ''; + $reactionTo = $message['reaction']['message_id'] ?? ''; + $messageText = json_encode(['emoji' => $emoji, 'message_id' => $reactionTo]); + } elseif (isset($message['text'])) { $messageText = $message['text']['body']; $messageType = 'text'; } elseif (isset($message['image'])) { diff --git a/assets/js/app.js b/assets/js/app.js index 09c5114..40265e9 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1028,6 +1028,38 @@ class WhatsAppBotManager { this.showTab('messages'); } + replyToMessage(messageId, userPhone) { + // Poner en el input del chat y almacenar reply_to en data attribute + document.getElementById('message-input').focus(); + document.getElementById('message-input').dataset.replyTo = messageId; + this.showSuccess('Preparado para responder al mensaje ' + messageId); + } + + async reactToMessage(messageId, userPhone) { + const emoji = prompt('Escribe el emoji de reacción (ej: ❤️, 👍):'); + if (!emoji) return; + + try { + const resp = await fetch('api/send_reaction.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ to: userPhone, message_id: messageId, emoji: emoji }) + }); + const data = await resp.json(); + if (data.success) { + this.showSuccess('Reacción enviada'); + // Refrescar mensajes + await this.loadMessages(this.currentUserId, false); + } else { + this.showError('Error enviando reacción: ' + (data.error || 'desconocido')); + } + } catch (err) { + console.error('reactToMessage error', err); + this.showError('Error comunicándose con el servidor'); + } + } + + editUser(userId) { // Implementar modal de edición de usuario console.log('Editar usuario:', userId); diff --git a/config/config_enhanced.php b/config/config_enhanced.php index a4b8812..c7f8e7f 100644 --- a/config/config_enhanced.php +++ b/config/config_enhanced.php @@ -44,50 +44,50 @@ function env($key, $default = null) { } // Configuración de la base de datos -define('DB_HOST', env('DB_HOST', 'localhost')); -define('DB_PORT', env('DB_PORT', '3306')); -define('DB_NAME', env('DB_NAME', 'whatsapp_bot')); -define('DB_USER', env('DB_USER', 'whatsapp_user')); -define('DB_PASS', env('DB_PASS', '')); // Se generará automáticamente en la instalación -define('DB_CHARSET', env('DB_CHARSET', 'utf8mb4')); +if (!defined('DB_HOST')) define('DB_HOST', env('DB_HOST', 'localhost')); +if (!defined('DB_PORT')) define('DB_PORT', env('DB_PORT', '3306')); +if (!defined('DB_NAME')) define('DB_NAME', env('DB_NAME', 'whatsapp_bot')); +if (!defined('DB_USER')) define('DB_USER', env('DB_USER', 'whatsapp_user')); +if (!defined('DB_PASS')) define('DB_PASS', env('DB_PASS', '')); // Se generará automáticamente en la instalación +if (!defined('DB_CHARSET')) define('DB_CHARSET', env('DB_CHARSET', 'utf8mb4')); // WhatsApp Business API -define('WHATSAPP_TOKEN', env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI')); -define('WHATSAPP_PHONE_NUMBER_ID', env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI')); -define('WHATSAPP_API_URL', env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/')); -define('WEBHOOK_VERIFY_TOKEN', env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123')); +if (!defined('WHATSAPP_TOKEN')) define('WHATSAPP_TOKEN', env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI')); +if (!defined('WHATSAPP_PHONE_NUMBER_ID')) define('WHATSAPP_PHONE_NUMBER_ID', env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI')); +if (!defined('WHATSAPP_API_URL')) define('WHATSAPP_API_URL', env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/')); +if (!defined('WEBHOOK_VERIFY_TOKEN')) define('WEBHOOK_VERIFY_TOKEN', env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123')); // Auto-approve templates: Si está activado, las plantillas creadas localmente se marcarán como 'approved' automáticamente -define('AUTO_APPROVE_TEMPLATES', filter_var(env('AUTO_APPROVE_TEMPLATES', 'false'), FILTER_VALIDATE_BOOLEAN)); +if (!defined('AUTO_APPROVE_TEMPLATES')) define('AUTO_APPROVE_TEMPLATES', filter_var(env('AUTO_APPROVE_TEMPLATES', 'false'), FILTER_VALIDATE_BOOLEAN)); // Configuración general -define('APP_NAME', env('APP_NAME', 'WhatsApp Bot System')); -define('APP_VERSION', env('APP_VERSION', '1.0.0')); -define('APP_URL', env('APP_URL', 'https://tudominio.com')); // Se auto-detectará -define('TIMEZONE', env('TIMEZONE', 'America/Bogota')); +if (!defined('APP_NAME')) define('APP_NAME', env('APP_NAME', 'WhatsApp Bot System')); +if (!defined('APP_VERSION')) define('APP_VERSION', env('APP_VERSION', '1.0.0')); +if (!defined('APP_URL')) define('APP_URL', env('APP_URL', 'https://tudominio.com')); +if (!defined('TIMEZONE')) define('TIMEZONE', env('TIMEZONE', 'America/Bogota')); // Información del desarrollador -define('DEVELOPER_NAME', 'U-Site.app'); -define('DEVELOPER_URL', 'https://u-site.app'); -define('DEVELOPER_EMAIL', 'support@u-site.app'); -define('DEVELOPER_SUPPORT', 'https://u-site.app/support'); +if (!defined('DEVELOPER_NAME')) define('DEVELOPER_NAME', 'U-Site.app'); +if (!defined('DEVELOPER_URL')) define('DEVELOPER_URL', 'https://u-site.app'); +if (!defined('DEVELOPER_EMAIL')) define('DEVELOPER_EMAIL', 'support@u-site.app'); +if (!defined('DEVELOPER_SUPPORT')) define('DEVELOPER_SUPPORT', 'https://u-site.app/support'); // Configuración de seguridad -define('INSTALLATION_LOCK', '.installation_completed'); // Archivo de bloqueo -define('ADMIN_USERNAME', env('ADMIN_USERNAME', 'admin')); // Usuario administrador por defecto -define('ADMIN_PASSWORD', env('ADMIN_PASSWORD', '')); // Se configurará en instalación -define('SESSION_TIMEOUT', (int)env('SESSION_TIMEOUT', 1800)); // 30 minutos -define('MAX_LOGIN_ATTEMPTS', (int)env('MAX_LOGIN_ATTEMPTS', 3)); // Intentos máximos de login -define('LOGIN_LOCKOUT_TIME', (int)env('LOGIN_LOCKOUT_TIME', 900)); // 15 minutos de bloqueo tras fallos +if (!defined('INSTALLATION_LOCK')) define('INSTALLATION_LOCK', '.installation_completed'); +if (!defined('ADMIN_USERNAME')) define('ADMIN_USERNAME', env('ADMIN_USERNAME', 'admin')); +if (!defined('ADMIN_PASSWORD')) define('ADMIN_PASSWORD', env('ADMIN_PASSWORD', '')); +if (!defined('SESSION_TIMEOUT')) define('SESSION_TIMEOUT', (int)env('SESSION_TIMEOUT', 1800)); +if (!defined('MAX_LOGIN_ATTEMPTS')) define('MAX_LOGIN_ATTEMPTS', (int)env('MAX_LOGIN_ATTEMPTS', 3)); +if (!defined('LOGIN_LOCKOUT_TIME')) define('LOGIN_LOCKOUT_TIME', (int)env('LOGIN_LOCKOUT_TIME', 900)); // Configuración de logs -define('ENABLE_LOGGING', filter_var(env('ENABLE_LOGGING', 'true'), FILTER_VALIDATE_BOOLEAN)); -define('LOG_LEVEL', env('LOG_LEVEL', 'INFO')); // DEBUG, INFO, WARNING, ERROR -define('LOG_FILE', env('LOG_FILE', 'logs/system.log')); +if (!defined('ENABLE_LOGGING')) define('ENABLE_LOGGING', filter_var(env('ENABLE_LOGGING', 'true'), FILTER_VALIDATE_BOOLEAN)); +if (!defined('LOG_LEVEL')) define('LOG_LEVEL', env('LOG_LEVEL', 'INFO')); // DEBUG, INFO, WARNING, ERROR +if (!defined('LOG_FILE')) define('LOG_FILE', env('LOG_FILE', 'logs/system.log')); // Configuración de archivos -define('UPLOAD_MAX_SIZE', (int)env('UPLOAD_MAX_SIZE', 10485760)); // 10MB -define('ALLOWED_FILE_TYPES', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'mp4', 'mp3', 'wav']); +if (!defined('UPLOAD_MAX_SIZE')) define('UPLOAD_MAX_SIZE', (int)env('UPLOAD_MAX_SIZE', 10485760)); // 10MB +if (!defined('ALLOWED_FILE_TYPES')) define('ALLOWED_FILE_TYPES', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'mp4', 'mp3', 'wav']); // Configurar zona horaria date_default_timezone_set(TIMEZONE); diff --git a/conversations.php b/conversations.php index 7112de0..2c6f25c 100644 --- a/conversations.php +++ b/conversations.php @@ -748,6 +748,10 @@
${content} +
+ + +
${time} ${msg.direction === 'outgoing' ? `${statusIcon}` : ''} @@ -785,6 +789,7 @@ document.getElementById('send-btn').disabled = true; try { + const replyTo = input.dataset.replyTo || null; const response = await fetch('api/send_message.php', { method: 'POST', headers: { @@ -792,9 +797,12 @@ }, body: JSON.stringify({ user_id: this.currentUserId, - message: message + message: message, + reply_to: replyTo }) }); + // Clear reply data attribute + input.dataset.replyTo = null; const result = await response.json(); diff --git a/scripts/alter_conversations_status_safe.php b/scripts/alter_conversations_status_safe.php new file mode 100644 index 0000000..a60c4ae --- /dev/null +++ b/scripts/alter_conversations_status_safe.php @@ -0,0 +1,32 @@ +fetchAll("SHOW COLUMNS FROM conversations LIKE 'status'"); + print_r($rows); + + $distinct = $db->fetchAll("SELECT DISTINCT status FROM conversations"); + echo "Distinct status values in table:\n"; + print_r(array_column($distinct, 'status')); + + // Ask user confirmation (CLI only) + if (php_sapi_name() === 'cli') { + echo "About to alter column 'status' to ENUM('sent','delivered','read','failed','received') DEFAULT 'sent'. Continue? (y/N): "; + $resp = trim(fgets(STDIN)); + if (strtolower($resp) !== 'y') { + echo "Aborted by user.\n"; + exit; + } + } + + $db->query("ALTER TABLE conversations MODIFY COLUMN status ENUM('sent','delivered','read','failed','received') DEFAULT 'sent'"); + echo "ALTER OK\n"; +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} diff --git a/scripts/api_send_reaction_test.php b/scripts/api_send_reaction_test.php new file mode 100644 index 0000000..266f82a --- /dev/null +++ b/scripts/api_send_reaction_test.php @@ -0,0 +1,15 @@ + '573001234567', + 'message_id' => '', + 'emoji' => '👍', + 'dry_run' => true +]); + +file_put_contents('php://memory', $payload); // no effect; instead call service directly + +require_once __DIR__ . '/../api/send_reaction.php'; diff --git a/scripts/api_send_reply_test.php b/scripts/api_send_reply_test.php new file mode 100644 index 0000000..ca679e7 --- /dev/null +++ b/scripts/api_send_reply_test.php @@ -0,0 +1,5 @@ +sendReaction('573001234567', '', '❤️', true); + print_r($r); + + echo "Dry-run text reply:\n"; + $rr = $s->sendTextReply('573001234567', '', 'Gracias por tu mensaje', false, true); + print_r($rr); + +} catch (Throwable $t) { + echo "ERROR: " . $t->getMessage() . "\n"; +} diff --git a/scripts/webhook_post_test.php b/scripts/webhook_post_test.php new file mode 100644 index 0000000..09997d1 --- /dev/null +++ b/scripts/webhook_post_test.php @@ -0,0 +1,102 @@ + "whatsapp_business_account", + "entry" => [ + [ + "id" => "0", + "changes" => [ + [ + "field" => "messages", + "value" => [ + "messaging_product" => "whatsapp", + "metadata" => [ + "display_phone_number" => "16505551111", + "phone_number_id" => "123456123" + ], + "contacts" => [ + [ + "profile" => ["name" => "test user name"], + "wa_id" => "16315551181" + ] + ], + "messages" => [ + [ + "from" => "16315551181", + "id" => "ABGGFlA5Fpa", + "timestamp" => "1504902988", + "type" => "text", + "text" => ["body" => "this is a text message"] + ] + ] + ] + ] + ] + ] + ] +]; + +try { + $w = new WhatsAppWebhook(); + echo "Webhook instance created\n"; + $result = $w->processPayload($payload); + echo "processPayload result: " . ($result ? 'true' : 'false') . "\n"; + + // Simular reacción entrante + $reactionPayload = [ + "object" => "whatsapp_business_account", + "entry" => [ + [ + "id" => "0", + "changes" => [ + [ + "field" => "messages", + "value" => [ + "messaging_product" => "whatsapp", + "metadata" => [ + "display_phone_number" => "16505551111", + "phone_number_id" => "123456123" + ], + "contacts" => [ + [ + "profile" => ["name" => "reactor"], + "wa_id" => "16315551181" + ] + ], + "messages" => [ + [ + "from" => "16315551181", + "id" => "REACTION1", + "timestamp" => "1504902990", + "type" => "reaction", + "reaction" => ["message_id" => "ABGGFlA5Fpa", "emoji" => "❤️"] + ] + ] + ] + ] + ] + ] + ] + ]; + + $r2 = $w->processPayload($reactionPayload); + echo "processReaction result: " . ($r2 ? 'true' : 'false') . "\n"; + +} catch (Throwable $t) { + echo "Fatal: " . $t->getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} + +// Show last lines of system log +$log = __DIR__ . '/../logs/system.log'; +if (file_exists($log)) { + echo "-- Last log lines --\n"; + $lines = array_slice(file($log), -40); + foreach ($lines as $line) echo $line; +} else { + echo "No log file found\n"; +} diff --git a/services/WhatsAppService.php b/services/WhatsAppService.php index 8942401..a5bf3e9 100644 --- a/services/WhatsAppService.php +++ b/services/WhatsAppService.php @@ -536,10 +536,62 @@ class WhatsAppService return $data['interactive']['body']['text']; } break; + case 'reaction': + return isset($data['reaction']['emoji']) ? $data['reaction']['emoji'] : json_encode($data['reaction']); } return json_encode($data); } + /** + * Enviar reacción (emoji) a un mensaje previo + * @param string $to Teléfono del destinatario + * @param string $messageId ID del mensaje al que se reacciona + * @param string $emoji Emoji de reacción + * @param bool $dryRun Si true, no hace la petición y devuelve el payload + */ + public function sendReaction($to, $messageId, $emoji, $dryRun = false) + { + $data = [ + 'messaging_product' => 'whatsapp', + 'to' => $this->formatPhoneNumber($to), + 'type' => 'reaction', + 'reaction' => [ + 'message_id' => $messageId, + 'emoji' => $emoji + ] + ]; + + if ($dryRun) return $data; + return $this->sendMessage($data); + } + + /** + * Enviar texto como respuesta (con contexto) a un mensaje existente + * @param string $to Teléfono del destinatario + * @param string $messageId ID del mensaje al que se responde + * @param string $text Contenido del mensaje de respuesta + * @param bool $preview_url Incluir preview_url en body.text + * @param bool $dryRun Si true, no hace la petición y devuelve el payload + */ + public function sendTextReply($to, $messageId, $text, $preview_url = false, $dryRun = false) + { + $data = [ + 'messaging_product' => 'whatsapp', + 'to' => $this->formatPhoneNumber($to), + 'type' => 'text', + 'context' => [ + 'message_id' => $messageId + ], + 'text' => [ + 'preview_url' => (bool)$preview_url, + 'body' => $text + ] + ]; + + if ($dryRun) return $data; + return $this->sendMessage($data); + } + /** * Obtener usuario por teléfono */