218 lines
7.3 KiB
PHP
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;
|
|
}
|
|
}
|