* @license GPL-2.0+ https://www.gnu.org/licenses/gpl-2.0.html * @link https://aisk.chat */ /** * AISK_Chat_Storage Class * * Handles all database operations for chat conversations and messages * * @category Class * @package AISK * @author AISK Team * @license GPL-2.0+ https://www.gnu.org/licenses/gpl-2.0.html * @link https://aisk.chat */ class AISK_Chat_Storage { private static $instance = null; private $wpdb; private $state_table; /** * Get singleton instance of the class * * @return AISK_Chat_Storage Instance of the class */ public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor */ private function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->state_table = $this->wpdb->prefix . 'aisk_user_states'; } /** * Creates a new conversation * * @param array $data Conversation data * * @since 1.0.0 * * @return string Generated conversation ID */ public function create_conversation( $data ) { // Generate a unique conversation ID $conversation_id = wp_generate_uuid4(); $insert_data = array_merge( [ 'conversation_id' => $conversation_id, 'created_at' => gmdate('c'), 'updated_at' => gmdate('c'), ], $data ); try { $result = $this->wpdb->insert( $this->wpdb->prefix . 'aisk_conversations', $insert_data ); if ($result === false) { throw new Exception('Failed to insert conversation: ' . $this->wpdb->last_error); } // Cache the new conversation $cache_key = 'aisk_conversation_' . $conversation_id; wp_cache_set($cache_key, $insert_data, 'aisk_chat_storage', 300); return $conversation_id; } catch (Exception $e) { throw $e; } } private function invalidate_conversation_cache($conversation_id) { wp_cache_delete('aisk_messages_' . $conversation_id, 'aisk_chat'); } /** * Adds a message to a conversation * * @param string $conversation_id The conversation ID * @param string $message_type The type of message * @param string $message The message content * @param array $metadata Optional metadata for the message * * @since 1.0.0 * * @return int The inserted message ID */ public function add_message( $conversation_id, $message_type, $message, $metadata = null ) { // Validate message type if (!in_array($message_type, ['user', 'bot'])) { $message_type = 'bot'; // Default to bot if invalid } // Handle message content - ensure it's a string $message_content = ''; if (is_array($message)) { if (isset($message['error']) && isset($message['error']['message'])) { $message_content = $message['error']['message']; } elseif (isset($message['message'])) { $message_content = $message['message']; } else { $message_content = json_encode($message); } } elseif (is_wp_error($message)) { $message_content = $message->get_error_message(); } else { $message_content = (string)$message; } // Check cache for last message $cache_key = 'aisk_last_message_' . $conversation_id; $last_message = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $last_message) { // @codingStandardsIgnoreStart global $wpdb; $last_message = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aisk_messages WHERE conversation_id = %s ORDER BY created_at DESC LIMIT 1", $conversation_id ) ); wp_cache_set($cache_key, $last_message, 'aisk_chat_storage', 300); } // If the last message is identical, don't insert if ($last_message && $last_message->message_type === $message_type && $last_message->message === $message_content) { return $last_message->id; } $message_data = [ 'conversation_id' => $conversation_id, 'message_type' => $message_type, 'message' => $message_content, 'metadata' => $metadata ? json_encode($metadata) : null, 'created_at' => gmdate('c'), ]; $result = $this->wpdb->insert( $this->wpdb->prefix . 'aisk_messages', $message_data ); if ($result === false) { return false; } $message_id = $this->wpdb->insert_id; // Update conversation's updated_at timestamp $this->wpdb->update( $this->wpdb->prefix . 'aisk_conversations', ['updated_at' => gmdate('c')], ['conversation_id' => $conversation_id] ); // Clear related caches wp_cache_delete('aisk_messages_' . $conversation_id, 'aisk_chat_storage'); wp_cache_delete('aisk_messages_' . $conversation_id . '_15', 'aisk_chat_storage'); wp_cache_delete('aisk_last_message_' . $conversation_id, 'aisk_chat_storage'); wp_cache_delete('aisk_conversation_' . $conversation_id, 'aisk_chat_storage'); return $message_id; } /** * Gets a specific conversation by ID * * @param string $conversation_id The conversation ID * * @since 1.0.0 * * @return object|null Conversation data or null if not found */ public function get_conversation( $conversation_id ) { $cache_key = 'aisk_conversation_'. $conversation_id; $conversation = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $conversation) { // @codingStandardsIgnoreStart global $wpdb; $conversation = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aisk_conversations WHERE conversation_id = %s", $conversation_id ) ); // @codingStandardsIgnoreEnd // Cache the results for 5 minutes if ($conversation) { wp_cache_set($cache_key, $conversation, 'aisk_chat_storage', 300); } else { // Cache null result for 1 minute to prevent repeated DB queries for non-existent records wp_cache_set($cache_key, null, 'aisk_chat_storage', 60); } } return $conversation; } /** * Gets all messages for a conversation * * @param string $conversation_id The conversation ID * * @since 1.0.0 * * @return array Array of message objects */ public function get_messages( $conversation_id ) { // Try to get from cache first $cache_key = 'aisk_messages_' . $conversation_id; $messages = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $messages) { global $wpdb; // @codingStandardsIgnoreStart $messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aisk_messages WHERE conversation_id = %s ORDER BY created_at ASC", $conversation_id ) ); // @codingStandardsIgnoreEnd if ($messages) { // Cache the results for 5 minutes wp_cache_set($cache_key, $messages, 'aisk_chat_storage', 300); } else { // Cache empty result for 1 minute wp_cache_set($cache_key, array(), 'aisk_chat_storage', 60); } } return $messages; } /** * Gets conversations for a specific user or IP address * * @param int $user_id Optional user ID * @param string $ip_address Optional IP address * @param int $limit Maximum number of conversations to return * * @since 1.0.0 * * @return array Array of conversation objects */ public function get_user_conversations( $user_id = null, $ip_address = null, $limit = 10 ) { $cache_key = 'aisk_user_conversations_'. $user_id . '_' . $ip_address . '_' . $limit; $conversations = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $conversations) { // @codingStandardsIgnoreStart global $wpdb; $query = "SELECT * FROM {$wpdb->prefix}aisk_conversations WHERE 1=1"; $query_args = array(); if ($user_id && $ip_address) { $query .= " AND (user_id = %d OR ip_address = %s)"; $query_args[] = $user_id; $query_args[] = $ip_address; } elseif ($user_id) { $query .= " AND user_id = %d"; $query_args[] = $user_id; } elseif ($ip_address) { $query .= " AND ip_address = %s"; $query_args[] = $ip_address; } $query .= " ORDER BY updated_at DESC LIMIT %d"; $query_args[] = $limit; $conversations = $wpdb->get_results( $wpdb->prepare($query, $query_args) ); // @codingStandardsIgnoreEnd if ($conversations) { wp_cache_set($cache_key, $conversations, 'aisk_chat_storage', 300); } else { wp_cache_set($cache_key, array(), 'aisk_chat_storage', 60); } } return $conversations; } /** * Gets all conversations with pagination and filters * * @param int $page Page number * @param int $per_page Items per page * @param array $filters Array of filters * * @since 1.0.0 * * @return array Array containing conversations, total count and page count */ public function get_all_conversations( $page = 1, $per_page = 20, $filters = [] ) { $cache_key = 'aisk_all_conversations_'. $page. '_'. $per_page. '_'. md5(serialize($filters)); $result = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $result) { // @codingStandardsIgnoreStart global $wpdb; $offset = ($page - 1) * $per_page; $query = "SELECT * FROM {$wpdb->prefix}aisk_conversations WHERE 1=1"; $count_query = "SELECT COUNT(*) FROM {$wpdb->prefix}aisk_conversations WHERE 1=1"; $query_args = array(); // Apply location filter if (!empty($filters['location_filter']) && 'all' !== $filters['location_filter']) { $query .= " AND country = %s"; $count_query .= " AND country = %s"; $query_args[] = $filters['location_filter']; } // Apply time filter if (!empty($filters['time_filter'])) { $interval = ''; switch ($filters['time_filter']) { case '7days': $interval = '7 DAY'; break; case '30days': $interval = '30 DAY'; break; case '90days': $interval = '90 DAY'; break; } if ($interval) { $query .= " AND created_at >= DATE_SUB(NOW(), INTERVAL {$interval})"; $count_query .= " AND created_at >= DATE_SUB(NOW(), INTERVAL {$interval})"; } } $query .= " ORDER BY created_at DESC LIMIT %d OFFSET %d"; $query_args[] = $per_page; $query_args[] = $offset; $conversations = $wpdb->get_results( $wpdb->prepare($query, $query_args) ); $total = $wpdb->get_var( $wpdb->prepare($count_query, array_slice($query_args, 0, -2)) ); // @codingStandardsIgnoreEnd $result = [ 'conversations' => $conversations, 'total' => (int) $total, 'pages' => ceil($total / $per_page), ]; wp_cache_set($cache_key, $result, 'aisk_chat_storage', 300); } return $result; } /** * Closes a conversation * * @param string $conversation_id The conversation ID * * @since 1.0.0 * * @return int|false Number of rows affected or false on error */ public function close_conversation( $conversation_id ) { // @codingStandardsIgnoreStart global $wpdb; $result = $wpdb->update( $wpdb->prefix . 'aisk_conversations', [ 'status' => 'closed' ], [ 'conversation_id' => $conversation_id ] ); // @codingStandardsIgnoreEnd if ($result !== false) { // Clear related caches wp_cache_delete('aisk_conversation_' . $conversation_id, 'aisk_chat_storage'); wp_cache_delete('aisk_messages_' . $conversation_id, 'aisk_chat_storage'); wp_cache_delete('aisk_messages_' . $conversation_id . '_15', 'aisk_chat_storage'); } return $result; } /** * Gets recent message history for a conversation * * @param string $conversation_id The conversation ID * @param int $limit Maximum number of messages to return * * @since 1.0.0 * * @return array Array of formatted messages */ public function get_recent_message_history($conversation_id, $limit = 15) { $cache_key = 'aisk_messages_' . $conversation_id . '_' . $limit; $messages = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $messages) { // @codingStandardsIgnoreStart global $wpdb; $messages = $wpdb->get_results( $wpdb->prepare( "SELECT message_type, message, metadata FROM {$wpdb->prefix}aisk_messages WHERE conversation_id = %s ORDER BY created_at DESC LIMIT %d", $conversation_id, $limit ) ); // @codingStandardsIgnoreEnd if ($messages) { wp_cache_set($cache_key, $messages, 'aisk_chat_storage', 300); } else { wp_cache_set($cache_key, array(), 'aisk_chat_storage', 60); } } // Reverse array to maintain chronological order $messages = array_reverse($messages); // Format messages for AI context return array_map( function ($msg) { return [ 'bot' => $msg->message_type, 'message' => $msg->message, ]; }, $messages ); } /** * Gets active WhatsApp conversation for a user * * @param string $wa_id WhatsApp user ID * * @since 1.0.0 * * @return object|null Conversation data or null if not found */ public function get_whatsapp_conversation($wa_id) { $cache_key = 'aisk_wa_conv_' . $wa_id; $conversation = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $conversation) { // @codingStandardsIgnoreStart global $wpdb; $conversation = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aisk_conversations WHERE user_phone = %s AND platform = 'whatsapp' ORDER BY created_at DESC LIMIT 1", $wa_id ) ); // @codingStandardsIgnoreEnd if ($conversation) { wp_cache_set($cache_key, $conversation, 'aisk_chat_storage', 300); } else { wp_cache_set($cache_key, null, 'aisk_chat_storage', 60); } } return $conversation; } /** * Gets active Telegram conversation for a user * * @param string $user_phone Telegram user phone * * @since 1.0.0 * * @return object|null Conversation data or null if not found */ public function get_telegram_conversation( $user_phone ) { $cache_key = 'aisk_telegram_conv_' . $user_phone; $conversation = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $conversation) { // @codingStandardsIgnoreStart global $wpdb; $conversation = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aisk_conversations WHERE user_phone = %s AND platform = 'telegram' ORDER BY created_at DESC LIMIT 1", $user_phone ) ); // @codingStandardsIgnoreEnd if ($conversation) { wp_cache_set($cache_key, $conversation, 'aisk_chat_storage', 300); } else { wp_cache_set($cache_key, null, 'aisk_chat_storage', 60); } } return $conversation; } /** * Sets user state data * * @param string $platform_user_id User ID for the platform * @param string $platform Platform identifier * @param array $state_data State data to store * * @since 1.0.0 * * @return int|false Number of rows affected or false on error */ public function set_user_state( $platform_user_id, $platform, $state_data ) { // @codingStandardsIgnoreStart global $wpdb; $result = $wpdb->replace( $this->state_table, [ 'platform_user_id' => $platform_user_id, 'platform' => $platform, 'state_data' => json_encode($state_data), ], [ '%s', '%s', '%s' ] ); // @codingStandardsIgnoreEnd if ($result !== false) { // Cache the state data $cache_key = 'aisk_user_state_' . $platform . '_' . $platform_user_id; wp_cache_set($cache_key, $state_data, 'aisk_chat_storage', 300); } return $result; } /** * Gets user state data * * @param string $platform_user_id User ID for the platform * @param string $platform Platform identifier * * @since 1.0.0 * * @return array|null State data or null if not found */ public function get_user_state( $platform_user_id, $platform ) { $cache_key = 'aisk_user_state_' . $platform . '_' . $platform_user_id; $state = wp_cache_get($cache_key, 'aisk_chat_storage'); if (false === $state) { // @codingStandardsIgnoreStart global $wpdb; $result = $wpdb->get_var( $wpdb->prepare( "SELECT state_data FROM {$wpdb->prefix}aisk_user_states WHERE platform_user_id = %s AND platform = %s", $platform_user_id, $platform ) ); // @codingStandardsIgnoreEnd if ($result) { $state = json_decode($result, true); wp_cache_set($cache_key, $state, 'aisk_chat_storage', 300); } else { wp_cache_set($cache_key, null, 'aisk_chat_storage', 60); } } return $state; } /** * Clears user state data * * @param string $platform_user_id User ID for the platform * @param string $platform Platform identifier * * @since 1.0.0 * * @return int|false Number of rows affected or false on error */ public function clear_user_state( $platform_user_id, $platform ) { // @codingStandardsIgnoreStart global $wpdb; $result = $wpdb->delete( $this->state_table, [ 'platform_user_id' => $platform_user_id, 'platform' => $platform, ], [ '%s', '%s' ] ); // @codingStandardsIgnoreEnd if ($result !== false) { // Clear the cached state data $cache_key = 'aisk_user_state_' . $platform . '_' . $platform_user_id; wp_cache_delete($cache_key, 'aisk_chat_storage'); } return $result; } }