up
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
/**
|
||||
* Gestor de estados de conversación en Redis
|
||||
* Mantiene estados temporales de chat para flujos conversacionales
|
||||
*/
|
||||
|
||||
namespace WhatsApp\Queue;
|
||||
|
||||
use Predis\Client as RedisClient;
|
||||
|
||||
class ConversationState {
|
||||
private $redis;
|
||||
private $prefix = 'chat:state:';
|
||||
private $defaultTTL = 3600; // 1 hora
|
||||
|
||||
public function __construct(RedisClient $redis = null) {
|
||||
$this->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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user