handle('web', $telefono, $textoUsuario); * // $reply es un array de strings a enviar */ class ChatBotEngine { // ───────────────────────────────────────────── // Punto de entrada principal // ───────────────────────────────────────────── /** * @param string $canal 'web' | 'telegram' * @param string $canalId teléfono o chat_id de Telegram * @param string $texto texto enviado por el usuario * @param string $nombre nombre del contacto (opcional) * @return string[] array de mensajes a enviar de vuelta */ public function handle(string $canal, string $canalId, string $texto, string $nombre = ''): array { $texto = trim($texto); // 1. Obtener o crear contacto $contact = ChatContact::firstOrCreate( ['canal' => $canal, 'canal_id' => $canalId], ['nombre' => $nombre ?: $canalId, 'telefono' => $canal === 'web' ? $canalId : null] ); if ($nombre && $contact->nombre !== $nombre) { $contact->update(['nombre' => $nombre]); } // 2. Obtener conversación activa o crear una nueva $conv = $contact->activeConversation(); if (! $conv) { $conv = ChatConversation::create([ 'contact_id' => $contact->id, 'canal' => $canal, 'estado' => 'bot', 'ultimo_mensaje_at' => now(), ]); } // 3. Guardar el mensaje del usuario ChatMessage::create([ 'conversation_id' => $conv->id, 'tipo' => 'usuario', 'contenido' => $texto, 'leido' => false, ]); $conv->update([ 'ultimo_mensaje_at' => now(), 'mensajes_no_leidos' => $conv->mensajes_no_leidos + 1, ]); // 4. Si la conversación está en modo agente, no responder automáticamente if ($conv->estado === 'agente') { return []; } // 5. Procesar con el árbol de menús $replies = $this->processMenu($conv, $texto); // 6. Guardar respuestas del bot foreach ($replies as $reply) { ChatMessage::create([ 'conversation_id' => $conv->id, 'tipo' => 'bot', 'contenido' => $reply, 'leido' => true, ]); } return $replies; } // ───────────────────────────────────────────── // Lógica de menús // ───────────────────────────────────────────── private function processMenu(ChatConversation $conv, string $texto): array { // Cargar menú actual (si existe) o el menú raíz $menu = $conv->menu_actual_id ? ChatMenu::find($conv->menu_actual_id) : ChatMenu::raiz(); if (! $menu) { return [$this->mensajeSinMenu()]; } // Buscar opción que coincida con lo que escribió el usuario $opcion = $menu->options() ->where('clave', strtolower($texto)) ->first(); if (! $opcion) { // Opción no reconocida — reenviar el menú actual return [$menu->mensaje . "\n\n" . $this->formatOpciones($menu)]; } return match($opcion->accion) { 'ir_menu' => $this->irAMenu($conv, $opcion->menu_destino_id), 'respuesta' => $this->respuestaSimple($conv, $opcion), 'agente' => $this->transferirAgente($conv), default => ['Lo siento, no pude procesar tu solicitud.'], }; } private function irAMenu(ChatConversation $conv, ?int $menuId): array { if (! $menuId) { return ['Error: menú de destino no configurado.']; } $menu = ChatMenu::find($menuId); if (! $menu || ! $menu->activo) { return ['Lo siento, esa opción no está disponible en este momento.']; } $conv->update(['menu_actual_id' => $menu->id]); return [$menu->mensaje . "\n\n" . $this->formatOpciones($menu)]; } private function respuestaSimple(ChatConversation $conv, $opcion): array { // Después de la respuesta, volver al menú raíz $raiz = ChatMenu::raiz(); $conv->update(['menu_actual_id' => $raiz?->id]); $replies = [$opcion->respuesta_texto]; if ($raiz) { $replies[] = $raiz->mensaje . "\n\n" . $this->formatOpciones($raiz); } return $replies; } private function transferirAgente(ChatConversation $conv): array { $conv->update(['estado' => 'agente', 'menu_actual_id' => null]); return [ ChatConfig::get('mensaje_transferencia', 'Un agente se comunicará contigo en breve. Por favor espera.' ) ]; } // ───────────────────────────────────────────── // Helpers de formato // ───────────────────────────────────────────── private function formatOpciones(\App\Models\ChatMenu $menu): string { $lines = []; foreach ($menu->options as $op) { $lines[] = "{$op->clave}. {$op->etiqueta}"; } return implode("\n", $lines); } private function mensajeSinMenu(): string { return ChatConfig::get( 'mensaje_bienvenida', 'Hola 👋 Bienvenido. Escribe *hola* para comenzar.' ); } // ───────────────────────────────────────────── // Envío hacia Telegram // ───────────────────────────────────────────── public function enviarTelegram(string $chatId, string $texto): bool { $token = ChatConfig::get('telegram_token'); if (! $token) { return false; } $url = "https://api.telegram.org/bot{$token}/sendMessage"; $payload = json_encode(['chat_id' => $chatId, 'text' => $texto, 'parse_mode' => 'Markdown']); $ctx = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => $payload, 'timeout' => 10, ], ]); $result = @file_get_contents($url, false, $ctx); return $result !== false; } }