WP_REST_Server::READABLE, 'callback' => [ $this, 'get_threads' ], 'permission_callback' => '__return_true', ], [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_thread' ], 'permission_callback' => '__return_true', ], ] ); register_rest_route( 'crop-logic/v1', '/threads/(?P[a-zA-Z0-9-_]+)/messages', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_messages' ], 'permission_callback' => '__return_true', ] ); register_rest_route( 'crop-logic/v1', '/threads/(?P[a-zA-Z0-9-_]+)/send', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_message' ], 'permission_callback' => '__return_true', ] ); register_rest_route( 'crop-logic/v1', '/threads/(?P[a-zA-Z0-9-_]+)', [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete_thread' ], 'permission_callback' => '__return_true', ] ); register_rest_route( 'crop-logic/v1', '/auth/verify', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'verify_auth' ], 'permission_callback' => '__return_true', ] ); foreach ( [ 'login' => 'login', 'register' => 'register_user', 'forgot-password' => 'forgot_password', 'reset-password' => 'reset_password' ] as $route => $method ) { register_rest_route( 'crop-logic/v1', '/auth/' . $route, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, $method ], 'permission_callback' => '__return_true', ] ); } } public function get_threads( WP_REST_Request $request ) { $options = Settings::get_options(); $response = $this->remote_request( 'GET', $this->build_remote_url( $options['endpoint_url'], $options['list_threads_path'] ), [], $options, $this->get_user_token( $request ) ); if ( is_wp_error( $response ) ) { return $response; } return new WP_REST_Response( [ 'threads' => $this->normalize_threads_response( $response ) ], 200 ); } public function create_thread( WP_REST_Request $request ) { $options = Settings::get_options(); $body = [ 'title' => sanitize_text_field( (string) $request->get_param( 'title' ) ), 'farm_context' => [ 'source' => 'landing' ], ]; $response = $this->remote_request( 'POST', $this->build_remote_url( $options['endpoint_url'], $options['create_thread_path'] ), $body, $options, $this->get_user_token( $request ) ); if ( is_wp_error( $response ) ) { return $response; } $thread = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : $response; return new WP_REST_Response( [ 'thread' => $this->normalize_thread( $thread ) ], 200 ); } public function get_messages( WP_REST_Request $request ) { $options = Settings::get_options(); $conversation_id = sanitize_text_field( (string) $request['conversation_id'] ); $url = $this->build_remote_url( $options['endpoint_url'], $this->replace_path_tokens( $options['get_messages_path'], [ '{conversation_id}' => rawurlencode( $conversation_id ) ] ) ); $response = $this->remote_request( 'GET', $url, [], $options, $this->get_user_token( $request ) ); if ( is_wp_error( $response ) ) { return $response; } $messages = isset( $response['data']['messages'] ) && is_array( $response['data']['messages'] ) ? $response['data']['messages'] : []; $normalized = []; foreach ( $messages as $message ) { $item = $this->normalize_remote_message( $message ); if ( ! empty( $item ) ) { $normalized[] = $item; } } return new WP_REST_Response( [ 'messages' => $normalized, 'conversation_id' => $conversation_id ], 200 ); } public function create_message( WP_REST_Request $request ) { $limited = $this->check_rate_limit(); if ( is_wp_error( $limited ) ) { return $limited; } $options = Settings::get_options(); $conversation_id = sanitize_text_field( (string) $request['conversation_id'] ); $content = trim( (string) $request->get_param( 'message' ) ); $attachments = $this->normalize_attachments( $request->get_param( 'attachments' ) ); $token = $this->get_user_token( $request ); $verify = $this->perform_verify( $options, $token ); if ( is_wp_error( $verify ) ) { return $verify; } if ( ! $this->verify_is_authenticated( $verify ) ) { return new WP_REST_Response( [ 'requires_auth' => true, 'verify' => $verify, 'message' => __( 'برای ادامه ابتدا وارد حساب خود شوید.', 'crop-logic' ) ], 401 ); } if ( '' === $content && empty( $attachments ) ) { return new WP_Error( 'crop_logic_empty_message', __( 'حداقل متن یا تصویر باید ارسال شود.', 'crop-logic' ), [ 'status' => 400 ] ); } $payload = [ 'content' => $content, 'images' => array_values( array_map( [ $this, 'attachment_to_image_payload' ], $attachments ) ), 'conversation_id' => $conversation_id, 'farm_context' => [ 'source' => 'landing', 'page' => 'home', ], ]; if ( '' === $conversation_id ) { unset( $payload['conversation_id'] ); $payload['title'] = __( 'گفتگوی لندینگ', 'crop-logic' ); } $task_response = $this->remote_request( 'POST', $this->build_remote_url( $options['endpoint_url'], $options['create_message_path'] ), $payload, $options, $token ); if ( is_wp_error( $task_response ) ) { return $task_response; } $task_data = isset( $task_response['data'] ) && is_array( $task_response['data'] ) ? $task_response['data'] : []; $task_id = (string) ( $task_data['task_id'] ?? '' ); if ( '' === $task_id ) { return new WP_Error( 'crop_logic_missing_task_id', __( 'شناسه task از بک‌اند دریافت نشد.', 'crop-logic' ), [ 'status' => 500 ] ); } $status_data = $this->poll_task_status( $options, $token, $task_id ); if ( is_wp_error( $status_data ) ) { return $status_data; } $result = isset( $status_data['result'] ) && is_array( $status_data['result'] ) ? $status_data['result'] : []; $final_conv_id = (string) ( $result['conversation_id'] ?? $task_data['conversation_id'] ?? $conversation_id ); $user_message_id = (string) ( $task_data['message_id'] ?? '' ); $assistant = [ 'id' => (string) ( $result['message_id'] ?? uniqid( 'msg_', true ) ), 'role' => 'assistant', 'content' => (string) ( $result['content'] ?? '' ), 'sections' => isset( $result['sections'] ) && is_array( $result['sections'] ) ? $this->sanitize_sections( $result['sections'] ) : [], 'createdAt' => time() * 1000, ]; $user = [ 'id' => $user_message_id ?: uniqid( 'msg_', true ), 'role' => 'user', 'content' => $content, 'attachments' => array_values( array_map( [ $this, 'attachment_to_front_attachment' ], $attachments ) ), 'createdAt' => time() * 1000, 'sections' => [], ]; return new WP_REST_Response( [ 'conversation_id' => $final_conv_id, 'task_id' => $task_id, 'user_message' => $user, 'assistant_message' => $assistant, ], 200 ); } public function delete_thread( WP_REST_Request $request ) { $options = Settings::get_options(); $conversation_id = sanitize_text_field( (string) $request['conversation_id'] ); $url = $this->build_remote_url( $options['endpoint_url'], $this->replace_path_tokens( $options['delete_thread_path'], [ '{conversation_id}' => rawurlencode( $conversation_id ) ] ) ); $response = $this->remote_request( 'DELETE', $url, [], $options, $this->get_user_token( $request ) ); if ( is_wp_error( $response ) ) { return $response; } return new WP_REST_Response( [ 'deleted' => true, 'conversation_id' => $conversation_id ], 200 ); } public function verify_auth( WP_REST_Request $request ) { $options = Settings::get_options(); $verify = $this->perform_verify( $options, $this->get_user_token( $request ) ); if ( is_wp_error( $verify ) ) { return $verify; } return new WP_REST_Response( $verify, 200 ); } public function login( WP_REST_Request $request ) { return $this->handle_auth_proxy( 'login_path', $request ); } public function register_user( WP_REST_Request $request ) { return $this->handle_auth_proxy( 'register_path', $request ); } public function forgot_password( WP_REST_Request $request ) { return $this->handle_auth_proxy( 'forgot_path', $request ); } public function reset_password( WP_REST_Request $request ) { return $this->handle_auth_proxy( 'reset_path', $request ); } private function handle_auth_proxy( string $path_key, WP_REST_Request $request ) { $options = Settings::get_options(); $body = $request->get_json_params(); if ( ! is_array( $body ) ) { $body = $request->get_params(); } $response = $this->remote_request( 'POST', $this->build_remote_url( $options['endpoint_url'], $options[ $path_key ] ), is_array( $body ) ? $body : [], $options, $this->get_user_token( $request ) ); if ( is_wp_error( $response ) ) { return $response; } return new WP_REST_Response( $this->normalize_auth_response( $response ), 200 ); } private function perform_verify( array $options, string $token ) { $response = $this->remote_request( 'POST', $this->build_remote_url( $options['endpoint_url'], $options['verify_path'] ), [], $options, $token ); if ( is_wp_error( $response ) ) { return $response; } return [ 'authenticated' => $this->verify_is_authenticated( $response ), 'data' => $response ]; } private function verify_is_authenticated( array $verify ): bool { if ( isset( $verify['authenticated'] ) ) { return (bool) $verify['authenticated']; } if ( isset( $verify['data']['authenticated'] ) ) { return (bool) $verify['data']['authenticated']; } if ( isset( $verify['data']['code'] ) && 200 === (int) $verify['data']['code'] ) { return true; } return false; } private function normalize_auth_response( array $response ): array { return [ 'token' => (string) ( $response['token'] ?? '' ), 'authenticated' => ! empty( $response['token'] ), 'data' => $response['data'] ?? [], 'message' => (string) ( $response['msg'] ?? $response['message'] ?? '' ), ]; } private function get_user_token( WP_REST_Request $request ): string { $token = (string) $request->get_header( 'X-Crop-Logic-Token' ); if ( '' === $token ) { $token = (string) $request->get_param( 'token' ); } return trim( $token ); } private function poll_task_status( array $options, string $token, string $task_id ) { $status_url = $this->build_remote_url( $options['endpoint_url'], $this->replace_path_tokens( $options['task_status_path'], [ '{task_id}' => rawurlencode( $task_id ) ] ) ); for ( $attempt = 0; $attempt < 10; $attempt++ ) { $response = $this->remote_request( 'GET', $status_url, [], $options, $token ); if ( is_wp_error( $response ) ) { return $response; } $data = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : []; $status = (string) ( $data['status'] ?? '' ); if ( 'SUCCESS' === $status ) { return $data; } if ( 'FAILURE' === $status ) { return new WP_Error( 'crop_logic_task_failed', (string) ( $data['error'] ?? __( 'پردازش پیام ناموفق بود.', 'crop-logic' ) ), [ 'status' => 500 ] ); } sleep( 2 ); } return new WP_Error( 'crop_logic_task_timeout', __( 'پاسخ AI در زمان مناسب دریافت نشد.', 'crop-logic' ), [ 'status' => 504 ] ); } private function normalize_threads_response( array $response ): array { $items = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : []; $threads = []; foreach ( $items as $item ) { $normalized = $this->normalize_thread( $item ); if ( ! empty( $normalized ) ) { $threads[] = $normalized; } } return $threads; } private function normalize_thread( $thread ): array { if ( ! is_array( $thread ) ) { return []; } $id = sanitize_text_field( (string) ( $thread['id'] ?? '' ) ); if ( '' === $id ) { return []; } return [ 'id' => $id, 'title' => sanitize_text_field( (string) ( $thread['title'] ?? __( 'گفتگو', 'crop-logic' ) ) ), 'updatedAt' => time() * 1000, 'messages' => [], ]; } private function normalize_remote_message( array $message ): array { return [ 'id' => sanitize_text_field( (string) ( $message['message_id'] ?? uniqid( 'msg_', true ) ) ), 'role' => in_array( $message['role'] ?? '', [ 'user', 'assistant', 'system' ], true ) ? $message['role'] : 'assistant', 'content' => wp_kses_post( (string) ( $message['content'] ?? '' ) ), 'createdAt' => $this->normalize_timestamp( $message['created_at'] ?? time() ), 'sections' => isset( $message['sections'] ) && is_array( $message['sections'] ) ? $this->sanitize_sections( $message['sections'] ) : [], 'attachments' => [], ]; } private function normalize_timestamp( $value ): int { if ( is_numeric( $value ) ) { return (int) $value; } $timestamp = strtotime( (string) $value ); return $timestamp ? $timestamp * 1000 : time() * 1000; } private function normalize_attachments( $attachments ): array { if ( ! is_array( $attachments ) ) { return []; } $normalized = []; foreach ( $attachments as $item ) { if ( ! is_array( $item ) ) { continue; } $name = sanitize_file_name( (string) ( $item['name'] ?? '' ) ); $mime = sanitize_text_field( (string) ( $item['type'] ?? '' ) ); $data = trim( (string) ( $item['data'] ?? '' ) ); if ( '' === $data ) { continue; } $normalized[] = [ 'name' => $name, 'mime' => $mime, 'data' => $data ]; } return array_slice( $normalized, 0, 4 ); } private function attachment_to_image_payload( array $attachment ): string { return (string) $attachment['data']; } private function attachment_to_front_attachment( array $attachment ): array { return [ 'name' => (string) $attachment['name'], 'type' => (string) $attachment['mime'] ]; } private function sanitize_sections( array $sections ): array { $allowed_types = [ 'text', 'list', 'recommendation', 'warning' ]; $allowed_icons = [ 'droplet', 'leaf', 'warning', 'fertilizer', 'calendar' ]; $cleaned = []; foreach ( $sections as $section ) { if ( ! is_array( $section ) ) { continue; } $type = in_array( $section['type'] ?? '', $allowed_types, true ) ? $section['type'] : 'text'; $icon = in_array( $section['icon'] ?? '', $allowed_icons, true ) ? $section['icon'] : 'leaf'; $cleaned[] = [ 'type' => $type, 'title' => sanitize_text_field( (string) ( $section['title'] ?? '' ) ), 'content' => wp_kses_post( (string) ( $section['content'] ?? '' ) ), 'items' => array_values( array_filter( array_map( 'sanitize_text_field', is_array( $section['items'] ?? null ) ? $section['items'] : [] ) ) ), 'icon' => $icon, 'frequency' => sanitize_text_field( (string) ( $section['frequency'] ?? '' ) ), 'amount' => sanitize_text_field( (string) ( $section['amount'] ?? '' ) ), 'timing' => sanitize_text_field( (string) ( $section['timing'] ?? '' ) ), 'expandableExplanation' => sanitize_text_field( (string) ( $section['expandableExplanation'] ?? '' ) ), ]; } return $cleaned; } private function build_remote_url( string $base, string $path ): string { return rtrim( $base, '/' ) . '/' . ltrim( $path, '/' ); } private function replace_path_tokens( string $path, array $tokens ): string { return strtr( $path, $tokens ); } private function remote_request( string $method, string $url, array $payload, array $options, string $user_token = '' ) { $headers = [ 'Content-Type' => 'application/json' ]; if ( '' !== trim( (string) $options['api_key'] ) ) { $headers['X-Server-Api-Key'] = trim( (string) $options['api_key'] ); } if ( '' !== $user_token ) { $headers[ trim( (string) $options['token_header'] ) ?: 'Authorization' ] = (string) $options['token_prefix'] . $user_token; } if ( '' !== trim( (string) $options['custom_headers'] ) ) { $custom = json_decode( (string) $options['custom_headers'], true ); if ( is_array( $custom ) ) { foreach ( $custom as $key => $value ) { if ( is_string( $key ) && ( is_scalar( $value ) || null === $value ) ) { $headers[ $key ] = (string) $value; } } } } $args = [ 'timeout' => max( 5, (int) $options['timeout'] ), 'headers' => $headers, 'method' => $method ]; if ( 'GET' === $method && ! empty( $payload ) ) { $url = add_query_arg( $payload, $url ); } elseif ( ! empty( $payload ) ) { $args['body'] = wp_json_encode( $payload ); } $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return new WP_Error( 'crop_logic_request_failed', $response->get_error_message(), [ 'status' => 500 ] ); } $code = wp_remote_retrieve_response_code( $response ); $body = (string) wp_remote_retrieve_body( $response ); if ( '' === $body && 'DELETE' === $method && $code >= 200 && $code < 300 ) { return []; } $decoded = json_decode( $body, true ); if ( $code < 200 || $code >= 300 ) { $message = is_array( $decoded ) ? (string) ( $decoded['msg'] ?? $decoded['message'] ?? __( 'بک‌اند خطا برگرداند.', 'crop-logic' ) ) : __( 'بک‌اند خطا برگرداند.', 'crop-logic' ); return new WP_Error( 'crop_logic_http_error', $message, [ 'status' => $code ?: 500 ] ); } if ( '' === $body ) { return []; } if ( ! is_array( $decoded ) ) { return new WP_Error( 'crop_logic_bad_json', __( 'پاسخ JSON بک‌اند معتبر نیست.', 'crop-logic' ), [ 'status' => 500 ] ); } return $decoded; } private function check_rate_limit() { $ip = sanitize_text_field( (string) ( $_SERVER['REMOTE_ADDR'] ?? 'guest' ) ); $key = 'crop_logic_rl_' . md5( $ip ); $data = get_transient( $key ); if ( ! is_array( $data ) ) { $data = [ 'count' => 0, 'time' => time() ]; } if ( (int) $data['count'] >= 20 ) { return new WP_Error( 'crop_logic_rate_limit', __( 'تعداد درخواست‌ها زیاد است. یک دقیقه دیگر دوباره تلاش کنید.', 'crop-logic' ), [ 'status' => 429 ] ); } $data['count'] = (int) $data['count'] + 1; set_transient( $key, $data, MINUTE_IN_SECONDS ); return true; } }