| 1 |
<?php |
| 2 |
/** Private, per-user and per-page conversations, independent of pairing credentials. */ |
| 3 |
if ( ! defined( 'ABSPATH' ) ) { exit; } |
| 4 |
function bkbg_chat_key( $user, $post ) { return 'bkbg_ai_chats_' . get_current_blog_id() . '_' . (int) $user . '_' . (int) $post; } |
| 5 |
function bkbg_chat_read( $user, $post ) { |
| 6 |
$value = get_option( bkbg_chat_key( $user, $post ) ); |
| 7 |
return is_array( $value ) ? $value : array( 'active' => '', 'chats' => array() ); |
| 8 |
} |
| 9 |
function bkbg_chat_change( $user, $post, $callback ) { |
| 10 |
global $wpdb; |
| 11 |
$key = bkbg_chat_key( $user, $post ); |
| 12 |
add_option( $key, array( 'active' => '', 'chats' => array() ), '', false ); |
| 13 |
for ( $i = 0; $i < 5; $i++ ) { |
| 14 |
wp_cache_delete( $key, 'options' ); $old = get_option( $key ); $next = $old; |
| 15 |
$result = $callback( $next ); |
| 16 |
if ( is_wp_error( $result ) ) { return $result; } |
| 17 |
if ( $next === $old ) { return $next; } |
| 18 |
if ( strlen( maybe_serialize( $next ) ) > 15000000 ) { return bkbg_external_error( 'Conversation storage is full.', 413 ); } |
| 19 |
$changed = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_value=%s WHERE option_name=%s AND BINARY option_value=BINARY %s", maybe_serialize( $next ), $key, maybe_serialize( $old ) ) ); |
| 20 |
wp_cache_delete( $key, 'options' ); |
| 21 |
if ( 1 === $changed ) { return $next; } |
| 22 |
} |
| 23 |
return bkbg_external_error( 'Chat changed concurrently. Retry saving.', 409 ); |
| 24 |
} |
| 25 |
function bkbg_chat_valid_id( $id ) { return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{8,80}$/D', $id ); } |
| 26 |
function bkbg_chat_save( $request ) { |
| 27 |
$chat = $request['chat']; |
| 28 |
if ( strlen( $request->get_body() ) > 2000000 || ! is_array( $chat ) || ! bkbg_chat_valid_id( $chat['id'] ?? null ) || ! is_array( $chat['messages'] ?? null ) || ! is_array( $chat['history'] ?? null ) ) { return bkbg_external_error( 'Invalid or oversized conversation.', 413 ); } |
| 29 |
foreach ( $chat['messages'] as $message ) { |
| 30 |
if ( ! is_array( $message ) || ! in_array( $message['role'] ?? '', array( 'user', 'assistant', 'error' ), true ) || ! is_string( $message['text'] ?? null ) ) { return bkbg_external_error( 'Invalid chat message.' ); } |
| 31 |
} |
| 32 |
$chat['title'] = sanitize_text_field( substr( $chat['title'] ?? 'Conversation', 0, 240 ) ); |
| 33 |
return bkbg_chat_change( get_current_user_id(), absint( $request['post_id'] ), function ( &$library ) use ( $chat ) { |
| 34 |
$id = $chat['id']; $old = $library['chats'][ $id ] ?? array(); |
| 35 |
// External messages may arrive while the editor saves. Preserve those IDs. |
| 36 |
$ids = array(); |
| 37 |
foreach ( $chat['messages'] as $index => $incoming ) { if ( isset( $incoming['id'] ) ) { $ids[ $incoming['id'] ] = $index; } } |
| 38 |
foreach ( $old['messages'] ?? array() as $message ) { |
| 39 |
if ( ! empty( $message['external'] ) ) { |
| 40 |
$index = $ids[ $message['id'] ] ?? null; |
| 41 |
if ( null === $index ) { $chat['messages'][] = $message; } |
| 42 |
elseif ( 'queued' !== ( $message['status'] ?? '' ) ) { $chat['messages'][ $index ] = $message; } |
| 43 |
} |
| 44 |
} |
| 45 |
$chat['updated'] = microtime( true ) * 1000; |
| 46 |
$library['chats'][ $id ] = $chat; $library['active'] = $id; |
| 47 |
} ); |
| 48 |
} |
| 49 |
function bkbg_chat_append( $s, $message ) { |
| 50 |
if ( empty( $s['chat_id'] ) ) { return true; } // Legacy sessions have no saved conversation. |
| 51 |
return bkbg_chat_change( $s['user'], $s['post'], function ( &$library ) use ( $s, $message ) { |
| 52 |
$id = $s['chat_id']; |
| 53 |
if ( empty( $library['chats'][ $id ] ) ) { $library['chats'][ $id ] = array( 'id' => $id, 'title' => 'External conversation', 'messages' => array(), 'history' => array() ); } |
| 54 |
$chat = &$library['chats'][ $id ]; |
| 55 |
foreach ( $chat['messages'] as &$old ) { if ( ( $old['id'] ?? '' ) === $message['id'] ) { $old = $message; $chat['updated'] = microtime( true ) * 1000; return; } } unset( $old ); |
| 56 |
$chat['messages'][] = $message; $chat['updated'] = microtime( true ) * 1000; |
| 57 |
} ); |
| 58 |
} |
| 59 |
function bkbg_chat_can_access( $request ) { |
| 60 |
if ( ! bkbg_ai_user_can_use() ) { |
| 61 |
return false; |
| 62 |
} |
| 63 |
$post_id = absint( $request['post_id'] ); |
| 64 |
return $post_id && get_post( $post_id ) && current_user_can( 'edit_post', $post_id ); |
| 65 |
} |
| 66 |
add_action( 'rest_api_init', function () { |
| 67 |
register_rest_route( 'blockenberg/v1', '/ai/conversations/(?P<post_id>\d+)', array( |
| 68 |
array( 'methods' => 'GET', 'callback' => function ( $r ) { return bkbg_chat_read( get_current_user_id(), absint( $r['post_id'] ) ); }, 'permission_callback' => 'bkbg_chat_can_access' ), |
| 69 |
array( 'methods' => 'POST', 'callback' => 'bkbg_chat_save', 'permission_callback' => 'bkbg_chat_can_access' ), |
| 70 |
) ); |
| 71 |
} ); |
| 72 |
|
| 73 |
add_filter( 'rest_post_dispatch', function ( $response, $server, $request ) { |
| 74 |
if ( strpos( $request->get_route(), '/blockenberg/v1/ai/conversations/' ) === 0 && $response instanceof WP_REST_Response ) { $response->header( 'Cache-Control', 'no-store, private' ); } |
| 75 |
return $response; |
| 76 |
}, 10, 3 ); |
| 77 |
|