redis = $redis ?? new RedisClient([ 'scheme' => getenv('REDIS_SCHEME') ?: 'tcp', 'host' => getenv('REDIS_HOST') ?: '127.0.0.1', 'port' => getenv('REDIS_PORT') ?: 6379, 'password' => getenv('REDIS_PASSWORD') ?: null, 'database' => getenv('REDIS_DB') ?: 0, ]); } /** * Establecer estado de conversación * * @param string $userId ID del usuario * @param string $state Estado actual (ej: 'waiting_name', 'selecting_option', 'uploading_document') * @param array $context Datos adicionales del contexto * @param int $ttl Tiempo de vida en segundos * @return bool */ public function setState(string $userId, string $state, array $context = [], int $ttl = null): bool { try { $key = $this->prefix . $userId; $ttl = $ttl ?? $this->defaultTTL; $data = [ 'state' => $state, 'context' => $context, 'updated_at' => time() ]; $this->redis->setex($key, $ttl, json_encode($data)); return true; } catch (\Exception $e) { error_log("Failed to set conversation state: " . $e->getMessage()); return false; } } /** * Obtener estado actual de conversación * * @param string $userId * @return array|null ['state' => string, 'context' => array, 'updated_at' => int] */ public function getState(string $userId): ?array { try { $key = $this->prefix . $userId; $data = $this->redis->get($key); if (!$data) { return null; } return json_decode($data, true); } catch (\Exception $e) { error_log("Failed to get conversation state: " . $e->getMessage()); return null; } } /** * Actualizar contexto sin cambiar el estado * * @param string $userId * @param array $context Datos a agregar/actualizar en el contexto * @return bool */ public function updateContext(string $userId, array $context): bool { try { $current = $this->getState($userId); if (!$current) { return false; } $current['context'] = array_merge($current['context'] ?? [], $context); $current['updated_at'] = time(); $key = $this->prefix . $userId; $ttl = $this->redis->ttl($key); // Mantener el TTL original if ($ttl > 0) { $this->redis->setex($key, $ttl, json_encode($current)); } else { $this->redis->setex($key, $this->defaultTTL, json_encode($current)); } return true; } catch (\Exception $e) { error_log("Failed to update conversation context: " . $e->getMessage()); return false; } } /** * Limpiar estado (finalizar conversación) * * @param string $userId * @return bool */ public function clearState(string $userId): bool { try { $key = $this->prefix . $userId; $this->redis->del([$key]); return true; } catch (\Exception $e) { error_log("Failed to clear conversation state: " . $e->getMessage()); return false; } } /** * Extender TTL del estado actual * * @param string $userId * @param int $additionalSeconds Segundos adicionales * @return bool */ public function extendTTL(string $userId, int $additionalSeconds): bool { try { $key = $this->prefix . $userId; $currentTTL = $this->redis->ttl($key); if ($currentTTL > 0) { $this->redis->expire($key, $currentTTL + $additionalSeconds); return true; } return false; } catch (\Exception $e) { error_log("Failed to extend conversation TTL: " . $e->getMessage()); return false; } } /** * Verificar si usuario está en un estado específico * * @param string $userId * @param string $expectedState * @return bool */ public function isInState(string $userId, string $expectedState): bool { $current = $this->getState($userId); return $current && $current['state'] === $expectedState; } /** * Obtener valor específico del contexto * * @param string $userId * @param string $key * @param mixed $default * @return mixed */ public function getContextValue(string $userId, string $key, $default = null) { $state = $this->getState($userId); if (!$state || !isset($state['context'][$key])) { return $default; } return $state['context'][$key]; } /** * Almacenar datos temporales (para flujos multi-paso) * * Ejemplo: Durante un proceso de registro que requiere nombre, email, teléfono * se van guardando los datos hasta completar el formulario * * @param string $userId * @param string $key * @param mixed $value * @return bool */ public function setTemporaryData(string $userId, string $key, $value): bool { try { $tempKey = $this->prefix . 'temp:' . $userId . ':' . $key; $this->redis->setex($tempKey, 1800, json_encode($value)); // 30 minutos return true; } catch (\Exception $e) { error_log("Failed to set temporary data: " . $e->getMessage()); return false; } } /** * Obtener datos temporales * * @param string $userId * @param string $key * @return mixed|null */ public function getTemporaryData(string $userId, string $key) { try { $tempKey = $this->prefix . 'temp:' . $userId . ':' . $key; $data = $this->redis->get($tempKey); if (!$data) { return null; } return json_decode($data, true); } catch (\Exception $e) { error_log("Failed to get temporary data: " . $e->getMessage()); return null; } } /** * Limpiar datos temporales * * @param string $userId * @param string $key Si no se especifica, limpia todos los datos temp del usuario * @return bool */ public function clearTemporaryData(string $userId, string $key = null): bool { try { if ($key) { $tempKey = $this->prefix . 'temp:' . $userId . ':' . $key; $this->redis->del([$tempKey]); } else { // Limpiar todos los datos temp del usuario $pattern = $this->prefix . 'temp:' . $userId . ':*'; $keys = $this->redis->keys($pattern); if (!empty($keys)) { $this->redis->del($keys); } } return true; } catch (\Exception $e) { error_log("Failed to clear temporary data: " . $e->getMessage()); return false; } } /** * Marcar usuario como "escribiendo..." (útil para UX) * * @param string $userId * @param int $ttl Segundos (típicamente 3-5 segundos) * @return bool */ public function setTyping(string $userId, int $ttl = 5): bool { try { $key = $this->prefix . 'typing:' . $userId; $this->redis->setex($key, $ttl, '1'); return true; } catch (\Exception $e) { error_log("Failed to set typing indicator: " . $e->getMessage()); return false; } } /** * Verificar si usuario está escribiendo * * @param string $userId * @return bool */ public function isTyping(string $userId): bool { try { $key = $this->prefix . 'typing:' . $userId; return (bool) $this->redis->exists($key); } catch (\Exception $e) { return false; } } /** * Obtener estadísticas de estados activos * * @return array */ public function getActiveStates(): array { try { $pattern = $this->prefix . '*'; $keys = $this->redis->keys($pattern); $states = []; foreach ($keys as $key) { // Excluir keys temporales y typing if (strpos($key, ':temp:') !== false || strpos($key, ':typing:') !== false) { continue; } $data = $this->redis->get($key); if ($data) { $decoded = json_decode($data, true); $state = $decoded['state'] ?? 'unknown'; if (!isset($states[$state])) { $states[$state] = 0; } $states[$state]++; } } return $states; } catch (\Exception $e) { error_log("Failed to get active states: " . $e->getMessage()); return []; } } }