table_name = $wpdb->prefix . "wpaw_conversations"; } /** * Generate a unique session ID * * @return string */ public function generate_session_id() { if (function_exists("wp_generate_uuid4")) { return str_replace("-", "", wp_generate_uuid4()); } // Fallback for older WP versions (UUID v4 format, without hyphens). return str_replace("-", "", sprintf( "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", wp_rand(0, 0xffff), wp_rand(0, 0xffff), wp_rand(0, 0xffff), wp_rand(0, 0x0fff) | 0x4000, wp_rand(0, 0x3fff) | 0x8000, wp_rand(0, 0xffff), wp_rand(0, 0xffff), wp_rand(0, 0xffff), )); } /** * Create a new conversation session * * @param array $data Session data. * @return string|WP_Error Session ID or error. */ public function create_session($data = []) { global $wpdb; $session_id = $this->generate_session_id(); $user_id = get_current_user_id(); $result = $wpdb->insert( $this->table_name, [ "session_id" => $session_id, "user_id" => $user_id, "post_id" => isset($data["post_id"]) ? (int) $data["post_id"] : 0, "title" => isset($data["title"]) ? sanitize_text_field($data["title"]) : "", "focus_keyword" => isset($data["focus_keyword"]) ? sanitize_text_field($data["focus_keyword"]) : "", "messages" => isset($data["messages"]) ? json_encode($data["messages"]) : "[]", "context" => isset($data["context"]) ? json_encode($data["context"]) : "{}", "status" => "active", ], ["%s", "%d", "%d", "%s", "%s", "%s", "%s", "%s"], ); if (false === $result) { return new WP_Error( "db_error", __("Failed to create session.", "wp-agentic-writer"), ["status" => 500], ); } $this->current_session_id = $session_id; return $session_id; } /** * Check if current user can access a session * * @param string $session_id Session ID. * @return bool True if user can access. */ public function current_user_can_access($session_id) { $session = $this->get_session($session_id); if (!$session) { return false; } $current_user_id = get_current_user_id(); // User owns this session if ((int) $session["user_id"] === $current_user_id) { return true; } // For post-linked sessions, check if user can edit the post if (!empty($session["post_id"])) { $post_id = (int) $session["post_id"]; if ($post_id > 0 && current_user_can("edit_post", $post_id)) { return true; } } return false; } /** * Get a session by session ID (with authorization check) * * @param string $session_id Session ID. * @return array|null Session data or null if not found/not authorized. */ public function get_session($session_id) { global $wpdb; $session = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE session_id = %s", $session_id, ), ARRAY_A, ); if (!$session) { return null; } // Decode JSON fields $session["messages"] = json_decode($session["messages"], true) ?: []; $session["context"] = json_decode($session["context"], true) ?: []; return $session; } /** * Get a session by session ID (public - for internal use only) * Use this only when authorization is handled separately * * @param string $session_id Session ID. * @return array|null Session data or null if not found. */ public function get_session_unchecked($session_id) { global $wpdb; $session = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE session_id = %s", $session_id, ), ARRAY_A, ); if (!$session) { return null; } // Decode JSON fields $session["messages"] = json_decode($session["messages"], true) ?: []; $session["context"] = json_decode($session["context"], true) ?: []; return $session; } /** * Get session by post ID * * @param int $post_id Post ID. * @return array|null Session data or null. */ public function get_session_by_post_id($post_id) { global $wpdb; $session = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE post_id = %d AND status != 'archived' ORDER BY updated_at DESC LIMIT 1", $post_id, ), ARRAY_A, ); if (!$session) { return null; } $session["messages"] = json_decode($session["messages"], true) ?: []; $session["context"] = json_decode($session["context"], true) ?: []; return $session; } /** * Get all active sessions for current user * * @param string $status Status filter (active, completed, archived). * @param int $limit Number of results. * @return array Sessions list. */ public function get_user_sessions($status = "active", $limit = 20) { global $wpdb; $user_id = get_current_user_id(); $posts_table = $wpdb->posts; $sessions = $wpdb->get_results( $wpdb->prepare( "SELECT c.id, c.session_id, c.post_id, c.title, c.focus_keyword, c.status, c.created_at, c.updated_at, JSON_LENGTH(c.messages) as message_count, COALESCE(p.post_status, '') as post_status FROM {$this->table_name} c LEFT JOIN {$posts_table} p ON p.ID = c.post_id WHERE c.user_id = %d AND c.status = %s ORDER BY updated_at DESC LIMIT %d", $user_id, $status, $limit, ), ARRAY_A, ); return $sessions ?: []; } /** * Get all uncompleted sessions (post_id = 0) for current user * * @param int $limit Number of results. * @return array Sessions list. */ public function get_uncompleted_sessions($limit = 20) { global $wpdb; $user_id = get_current_user_id(); $sessions = $wpdb->get_results( $wpdb->prepare( "SELECT id, session_id, post_id, title, focus_keyword, status, created_at, updated_at, JSON_LENGTH(messages) as message_count FROM {$this->table_name} WHERE user_id = %d AND post_id = 0 AND status = 'active' ORDER BY updated_at DESC LIMIT %d", $user_id, $limit, ), ARRAY_A, ); return $sessions ?: []; } /** * Update session messages * * @param string $session_id Session ID. * @param array $messages Messages array. * @return bool True on success. */ public function update_messages($session_id, $messages) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "messages" => json_encode($messages), "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%s", "%s"], ["%s"], ); return false !== $result; } /** * Update session context * * @param string $session_id Session ID. * @param array $context Context data. * @return bool True on success. */ public function update_context($session_id, $context) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "context" => json_encode($context), "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%s", "%s"], ["%s"], ); return false !== $result; } /** * Link session to a post * * @param string $session_id Session ID. * @param int $post_id Post ID. * @return bool True on success. */ public function link_to_post($session_id, $post_id) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "post_id" => (int) $post_id, "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%d", "%s"], ["%s"], ); return false !== $result; } /** * Update session title * * @param string $session_id Session ID. * @param string $title New title. * @return bool True on success. */ public function update_title($session_id, $title) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "title" => sanitize_text_field($title), "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%s", "%s"], ["%s"], ); return false !== $result; } /** * Update focus keyword * * @param string $session_id Session ID. * @param string $focus_keyword Focus keyword. * @return bool True on success. */ public function update_focus_keyword($session_id, $focus_keyword) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "focus_keyword" => sanitize_text_field($focus_keyword), "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%s", "%s"], ["%s"], ); return false !== $result; } /** * Mark session as completed * * @param string $session_id Session ID. * @return bool True on success. */ public function mark_completed($session_id) { global $wpdb; $result = $wpdb->update( $this->table_name, [ "status" => "completed", "updated_at" => current_time("mysql"), ], ["session_id" => $session_id], ["%s", "%s"], ["%s"], ); return false !== $result; } /** * Delete a session * * @param string $session_id Session ID. * @return bool True on success. */ public function delete_session($session_id) { global $wpdb; $result = $wpdb->delete( $this->table_name, ["session_id" => $session_id], ["%s"], ); return false !== $result; } /** * Get or create session for post * * @param int $post_id Post ID (can be 0 for new posts). * @return array Session data with session_id. */ public function get_or_create_session_for_post($post_id = 0) { // Try to find existing session for this post if ($post_id > 0) { $session = $this->get_session_by_post_id($post_id); if ($session) { return $session; } } // Create new session $session_id = $this->create_session(["post_id" => $post_id]); if (is_wp_error($session_id)) { return null; } return $this->get_session($session_id); } /** * Set current session ID * * @param string $session_id Session ID. */ public function set_current_session($session_id) { $this->current_session_id = $session_id; } /** * Get current session ID * * @return string|null */ public function get_current_session_id() { return $this->current_session_id; } /** * Get current session data * * @return array|null */ public function get_current_session() { if (!$this->current_session_id) { return null; } return $this->get_session($this->current_session_id); } /** * Check if current session has post ID * * @return bool */ public function current_session_has_post() { $session = $this->get_current_session(); return $session && $session["post_id"] > 0; } /** * Check if editor has content (for auto-save decision) * * @param int $post_id Post ID. * @return bool True if post has content blocks. */ public function post_has_content($post_id) { if ($post_id <= 0) { return false; } $post = get_post($post_id); if (!$post) { return false; } // Check if post has any blocks or content $blocks = parse_blocks($post->post_content); return !empty($blocks); } /** * Get all sessions for a specific post. * * @param int $post_id Post ID. * @return array Sessions array. */ public function get_sessions_for_post($post_id) { global $wpdb; if ($post_id <= 0) { return []; } $sessions = $wpdb->get_results( $wpdb->prepare( "SELECT *, JSON_LENGTH(messages) as message_count FROM {$this->table_name} WHERE post_id = %d ORDER BY updated_at DESC", $post_id, ), ARRAY_A, ); // Decode JSON fields for each session foreach ($sessions as &$session) { $session["messages"] = json_decode($session["messages"], true) ?: []; $session["context"] = json_decode($session["context"], true) ?: []; } return $sessions ?: []; } /** * Acquire or refresh a session edit lock. * * Lock format: "timestamp:user_id:tab_id" * A lock is stale if its timestamp is older than $window seconds. * * @param string $session_id Session ID. * @param string $tab_id Unique browser tab identifier. * @param int $window Lock expiry window in seconds (default 60). * @return array { locked: bool, holder?: array } */ public function acquire_lock($session_id, $tab_id, $window = 60) { global $wpdb; $user_id = get_current_user_id(); $now = time(); // Read current lock $current_lock = $wpdb->get_var( $wpdb->prepare( "SELECT edit_lock FROM {$this->table_name} WHERE session_id = %s", $session_id, ), ); if ($current_lock) { $parts = explode(":", $current_lock); $lock_time = (int) ($parts[0] ?? 0); $lock_user = (int) ($parts[1] ?? 0); $lock_tab = $parts[2] ?? ""; // Lock is still fresh AND held by a different tab if ($now - $lock_time < $window && $lock_tab !== $tab_id) { return [ "locked" => false, "holder" => [ "user_id" => $lock_user, "tab_id" => $lock_tab, "since" => $lock_time, ], ]; } } // Acquire / refresh the lock $lock_value = "{$now}:{$user_id}:{$tab_id}"; $wpdb->update( $this->table_name, ["edit_lock" => $lock_value], ["session_id" => $session_id], ["%s"], ["%s"], ); return ["locked" => true]; } /** * Release a session edit lock (only if held by this tab). * * @param string $session_id Session ID. * @param string $tab_id Browser tab identifier. * @return bool True if released. */ public function release_lock($session_id, $tab_id) { global $wpdb; $current_lock = $wpdb->get_var( $wpdb->prepare( "SELECT edit_lock FROM {$this->table_name} WHERE session_id = %s", $session_id, ), ); if (!$current_lock) { return true; } $parts = explode(":", $current_lock); $lock_tab = $parts[2] ?? ""; if ($lock_tab !== $tab_id) { return false; } $wpdb->update( $this->table_name, ["edit_lock" => null], ["session_id" => $session_id], ["%s"], ["%s"], ); return true; } /** * Check the lock status of a session without modifying it. * * @param string $session_id Session ID. * @param string $tab_id Current tab identifier. * @param int $window Lock expiry window in seconds. * @return array { is_locked: bool, is_mine: bool, holder?: array } */ public function check_lock($session_id, $tab_id = "", $window = 60) { global $wpdb; $now = time(); $current_lock = $wpdb->get_var( $wpdb->prepare( "SELECT edit_lock FROM {$this->table_name} WHERE session_id = %s", $session_id, ), ); if (!$current_lock) { return ["is_locked" => false, "is_mine" => false]; } $parts = explode(":", $current_lock); $lock_time = (int) ($parts[0] ?? 0); $lock_user = (int) ($parts[1] ?? 0); $lock_tab = $parts[2] ?? ""; // Expired if ($now - $lock_time >= $window) { return ["is_locked" => false, "is_mine" => false]; } $is_mine = $lock_tab === $tab_id; return [ "is_locked" => true, "is_mine" => $is_mine, "holder" => [ "user_id" => $lock_user, "tab_id" => $lock_tab, "since" => $lock_time, ], ]; } }