Files
sirpremiumv2/app/Services/ChatBotEngine.php
T
2026-04-30 21:32:32 -05:00

218 lines
7.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\ChatConfig;
use App\Models\ChatContact;
use App\Models\ChatConversation;
use App\Models\ChatMessage;
use App\Models\ChatMenu;
/**
* ChatBotEngine
*
* Procesa un mensaje entrante de cualquier canal y devuelve
* la respuesta que el bot debe enviar.
*
* Uso:
* $engine = new ChatBotEngine();
* $reply = $engine->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;
}
}