This commit is contained in:
lizandrogd
2026-01-21 01:02:17 -05:00
parent 4e3e02111b
commit 87b89b52a5
14 changed files with 387 additions and 36 deletions
+8 -1
View File
@@ -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',
+9 -3
View File
@@ -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':
+32
View File
@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../config/config.php';
header('Content-Type: application/json; charset=utf-8');
// Autenticación para APIs (debug skip)
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode && function_exists('requireAuthentication')) {
requireAuthentication();
}
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$to = $input['to'] ?? null;
$message_id = $input['message_id'] ?? null;
$emoji = $input['emoji'] ?? null;
$dry = isset($input['dry_run']) ? (bool)$input['dry_run'] : false;
if (!$to || !$message_id || !$emoji) {
http_response_code(400);
echo json_encode(['success' => 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()]);
}
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../config/config.php';
header('Content-Type: application/json; charset=utf-8');
// Autenticación para APIs (debug skip)
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode && function_exists('requireAuthentication')) {
requireAuthentication();
}
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$to = $input['to'] ?? null;
$message_id = $input['message_id'] ?? null;
$text = $input['text'] ?? null;
$preview = isset($input['preview_url']) ? (bool)$input['preview_url'] : false;
$dry = isset($input['dry_run']) ? (bool)$input['dry_run'] : false;
if (!$to || !$message_id || !$text) {
http_response_code(400);
echo json_encode(['success' => 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()]);
}
+7 -1
View File
@@ -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'])) {
+32
View File
@@ -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);
+30 -30
View File
@@ -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);
+9 -1
View File
@@ -748,6 +748,10 @@
<div class="message ${msg.direction}">
<div class="message-bubble">
${content}
<div class="message-actions mt-1">
<button class="btn btn-sm btn-link" onclick="app.replyToMessage('${msg.message_id}','${msg.user_phone}')" title="Responder"><i class="fas fa-reply"></i></button>
<button class="btn btn-sm btn-link" onclick="app.reactToMessage('${msg.message_id}','${msg.user_phone}')" title="Reaccionar"><i class="far fa-grin"></i></button>
</div>
<div class="message-time">
${time}
${msg.direction === 'outgoing' ? `<span class="message-status">${statusIcon}</span>` : ''}
@@ -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();
@@ -0,0 +1,32 @@
<?php
/**
* Safe migration: show current column type, backup distinct statuses, then alter.
*/
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
try {
$db = Database::getInstance();
echo "Current structure for conversations table:\n";
$rows = $db->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";
}
+15
View File
@@ -0,0 +1,15 @@
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
// Test calling API endpoint internally by setting input stream
$payload = json_encode([
'to' => '573001234567',
'message_id' => '<TEST_MSGID>',
'emoji' => '👍',
'dry_run' => true
]);
file_put_contents('php://memory', $payload); // no effect; instead call service directly
require_once __DIR__ . '/../api/send_reaction.php';
+5
View File
@@ -0,0 +1,5 @@
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
require_once __DIR__ . '/../api/send_reply.php';
+21
View File
@@ -0,0 +1,21 @@
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
require_once __DIR__ . '/../services/WhatsAppService.php';
try {
$s = new WhatsAppService();
echo "WhatsAppService loaded\n";
echo "Dry-run reaction:\n";
$r = $s->sendReaction('573001234567', '<MSGID123>', '❤️', true);
print_r($r);
echo "Dry-run text reply:\n";
$rr = $s->sendTextReply('573001234567', '<MSGID123>', 'Gracias por tu mensaje', false, true);
print_r($rr);
} catch (Throwable $t) {
echo "ERROR: " . $t->getMessage() . "\n";
}
+102
View File
@@ -0,0 +1,102 @@
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
require_once __DIR__ . '/../api/webhook.php';
$payload = [
"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" => "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";
}
+52
View File
@@ -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
*/