Files
wp-agentic-writer/includes/class-conversation-manager.php

557 lines
12 KiB
PHP

<?php
/**
* Conversation Manager
*
* Handles session-based chat history with MySQL table storage.
* Supports both post-linked and standalone sessions.
*
* @package WP_Agentic_Writer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WP_Agentic_Writer_Conversation_Manager {
/**
* Singleton instance
*
* @var WP_Agentic_Writer_Conversation_Manager
*/
private static $instance = null;
/**
* Database table name
*
* @var string
*/
private $table_name;
/**
* Current session ID
*
* @var string
*/
private $current_session_id = null;
/**
* Get singleton instance
*
* @return WP_Agentic_Writer_Conversation_Manager
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'wpaw_conversations';
}
/**
* Generate a unique session ID
*
* @return string
*/
public function generate_session_id() {
return substr( md5( uniqid( wp_rand(), true ) ), 0, 16 );
}
/**
* Create a new conversation session
*
* @param array $data Session data.
* @return string|WP_Error Session ID or error.
*/
public function create_session( $data = array() ) {
global $wpdb;
$session_id = $this->generate_session_id();
$user_id = get_current_user_id();
$result = $wpdb->insert(
$this->table_name,
array(
'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',
),
array( '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s' )
);
if ( false === $result ) {
return new WP_Error(
'db_error',
__( 'Failed to create session.', 'wp-agentic-writer' ),
array( '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 ) ?: array();
$session['context'] = json_decode( $session['context'], true ) ?: array();
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 ) ?: array();
$session['context'] = json_decode( $session['context'], true ) ?: array();
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 = 'active' ORDER BY updated_at DESC LIMIT 1",
$post_id
),
ARRAY_A
);
if ( ! $session ) {
return null;
}
$session['messages'] = json_decode( $session['messages'], true ) ?: array();
$session['context'] = json_decode( $session['context'], true ) ?: array();
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 ?: array();
}
/**
* 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 ?: array();
}
/**
* 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,
array(
'messages' => json_encode( $messages ),
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%s', '%s' ),
array( '%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,
array(
'context' => json_encode( $context ),
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%s', '%s' ),
array( '%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,
array(
'post_id' => (int) $post_id,
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%d', '%s' ),
array( '%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,
array(
'title' => sanitize_text_field( $title ),
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%s', '%s' ),
array( '%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,
array(
'focus_keyword' => sanitize_text_field( $focus_keyword ),
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%s', '%s' ),
array( '%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,
array(
'status' => 'completed',
'updated_at' => current_time( 'mysql' ),
),
array( 'session_id' => $session_id ),
array( '%s', '%s' ),
array( '%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,
array( 'session_id' => $session_id ),
array( '%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( array( '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 array();
}
$sessions = $wpdb->get_results(
$wpdb->prepare(
"SELECT * 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 ) ?: array();
$session['context'] = json_decode( $session['context'], true ) ?: array();
}
return $sessions ?: array();
}
}