request( 'GET', '/plugin/connection', null, self::TIMEOUT_DEFAULT, array() ); } /** * Фотосессия товара: `GET /plugin/photo-sessions/{id}`. Нет или чужая — NotFound. * * @param string $photo_session_id Id фотосессии из меты товара. * @return array Фотосессия (`PhotoSession`): в ней фото вещи `sourcePhotos[]` — id, слот, размер файла. * @throws Api_Exception Отказ. */ public function get_photo_session( string $photo_session_id ): array { return $this->request( 'GET', '/plugin/photo-sessions/' . rawurlencode( $photo_session_id ), null, self::TIMEOUT_DEFAULT, array( 'photo_session_id' => $photo_session_id ) ); } /** * Заводит фотосессию товара: `POST /plugin/photo-sessions`. * * @param int $product_id Id товара WooCommerce. * @param string $product_name Название товара, 1…255 знаков — станет названием фотосессии. * @param string $site_url Адрес сайта магазина — в происхождение фотосессии. * @param string $kind Тип фотосессии — одно из Photo_Kinds::ALL. * @return array Новая фотосессия (`PhotoSession`). * @throws Api_Exception Отказ. */ public function create_photo_session( int $product_id, string $product_name, string $site_url, string $kind ): array { return $this->request( 'POST', '/plugin/photo-sessions', array( 'productId' => $product_id, 'productName' => $product_name, 'siteUrl' => $site_url, 'kind' => $kind, ), self::TIMEOUT_DEFAULT, array( 'product_id' => $product_id, 'kind' => $kind, ) ); } /** * Загружает фото товара в слот: `PUT /plugin/photo-sessions/{id}/source-photos/{slot}`. * * Сервер может положить фото в другой, рекомендуемый слот — его надо брать из ответа. * * @param string $photo_session_id Id фотосессии. * @param string $slot Свободный слот — тот, что назвало окно. * @param string $file_name Имя файла — только для узнавания фото человеком. * @param string $mime_type Тип содержимого: image/jpeg, image/png или image/webp. * @param string $contents Байты файла. * @return array Фото вещи (`SourcePhoto`): его `id` и `slot`, куда оно легло на самом деле. * @throws Api_Exception Отказ; PhotoRejected — не принято именно это фото. */ public function upload_source_photo( string $photo_session_id, string $slot, string $file_name, string $mime_type, string $contents ): array { $boundary = Multipart::new_boundary(); return $this->request( 'PUT', '/plugin/photo-sessions/' . rawurlencode( $photo_session_id ) . '/source-photos/' . rawurlencode( $slot ), array( 'content_type' => Multipart::content_type( $boundary ), 'raw' => Multipart::file_body( $boundary, 'file', $file_name, $mime_type, $contents ), ), self::TIMEOUT_UPLOAD, array( 'photo_session_id' => $photo_session_id, 'slot' => $slot, 'bytes' => strlen( $contents ), ) ); } /** * Одноразовый пропуск в окно генерации: `POST /plugin/window-passes`. * * @param string $photo_session_id Фотосессия, которая откроется в окне. * @param int $product_id Id товара — для журнала сервиса. * @param string $image_size «1K» или «2K» — настройка плагина. * @param string $file_format «jpg» или «png» — настройка плагина. * @param string $aspect_ratio Формат кадров, например «3:4», — настройка плагина «Формат фото»: * окно ставит его форматом по умолчанию для фотосессии без кадров. * @param string $admin_origin Адрес страницы товара, с которой открыто окно, — редактора * в админке или витрины: только ему окно отправит свои сообщения. * @return array{windowUrl: string, expiresAt: string} * @throws Api_Exception Отказ. */ public function create_window_pass( string $photo_session_id, int $product_id, string $image_size, string $file_format, string $aspect_ratio, string $admin_origin ): array { return $this->request( 'POST', '/plugin/window-passes', array( 'photoSessionId' => $photo_session_id, 'productId' => $product_id, 'imageSize' => $image_size, 'fileFormat' => $file_format, 'aspectRatio' => $aspect_ratio, 'adminOrigin' => $admin_origin, ), self::TIMEOUT_DEFAULT, array( 'photo_session_id' => $photo_session_id, 'product_id' => $product_id, ) ); } /** * Готовый файл картинки: `POST /plugin/photo-sessions/{id}/images/{imageId}/files`. * * Бесплатно: за картинку заплачено, когда её делали. * * @param string $photo_session_id Id фотосессии. * @param string $image_id Id картинки — та, что ложится в товар. * @param string $file_format «jpg» или «png». * @param bool $watermark Ставить ли водяной знак. * @param bool $ai_label Ставить ли видимую пометку «создано ИИ». * @return array `ImageFile`: `url` — подписанная ссылка на файл, `fileName` — имя для медиатеки. * @throws Api_Exception Отказ. */ public function create_image_file( string $photo_session_id, string $image_id, string $file_format, bool $watermark, bool $ai_label ): array { return $this->request( 'POST', '/plugin/photo-sessions/' . rawurlencode( $photo_session_id ) . '/images/' . rawurlencode( $image_id ) . '/files', array( 'format' => $file_format, 'watermark' => $watermark, 'aiLabel' => $ai_label, ), self::TIMEOUT_FILE, array( 'photo_session_id' => $photo_session_id, 'image_id' => $image_id, ) ); } /** * Выполняет запрос и разбирает ответ. * * Перенаправления не выполняются: HTTP-клиент повторил бы заголовок с ключом на чужой * адрес, а API aiModelka не перенаправляет. Ответ 3xx считается неожиданным. * * @param string $method Метод HTTP. * @param string $path Путь после адреса API, со слешем в начале. * @param array|null $body Тело: массив — JSON; массив с ключами * content_type и raw — готовое тело как есть; * null — без тела. * @param int $timeout Сколько секунд ждать ответа. * @param array $log_context Подробности для журнала. * @return array Разобранный JSON ответа 2xx. * @throws Api_Exception Ответа нет, ответ не 2xx или тело не JSON. */ private function request( string $method, string $path, ?array $body, int $timeout, array $log_context ): array { $headers = array( 'Accept' => 'application/json', 'X-Aimodelka-Key' => $this->api_key, 'X-Aimodelka-Site' => $this->site_url, ); $args = array( 'method' => $method, 'timeout' => $timeout, 'redirection' => 0, 'user-agent' => 'aimodelka-woocommerce/' . AIMODELKA_VERSION . '; WordPress/' . get_bloginfo( 'version' ), ); if ( is_array( $body ) && isset( $body['raw'], $body['content_type'] ) ) { $headers['Content-Type'] = $body['content_type']; $args['body'] = $body['raw']; } elseif ( is_array( $body ) ) { $headers['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( $body ); } $args['headers'] = $headers; $log_context = array_merge( array( 'method' => $method, 'path' => $path, ), $log_context ); $started = microtime( true ); $response = wp_remote_request( $this->api_url . $path, $args ); $elapsed = (int) round( ( microtime( true ) - $started ) * 1000 ); $log_context['elapsed_ms'] = $elapsed; if ( is_wp_error( $response ) ) { $exception = new Api_Exception( Error_Kind::NoConnection, null, '', $response->get_error_code() . ': ' . $response->get_error_message() ); Logger::error( 'aiModelka API: no response', array_merge( $log_context, $exception->log_context() ) ); throw $exception; } $status = (int) wp_remote_retrieve_response_code( $response ); $decoded = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( $status >= 200 && $status < 300 ) { if ( ! is_array( $decoded ) ) { $exception = new Api_Exception( Error_Kind::Unexpected, $status, '', 'Response body is not JSON' ); Logger::error( 'aiModelka API: unreadable answer', array_merge( $log_context, $exception->log_context() ) ); throw $exception; } Logger::info( 'aiModelka API: ok', array_merge( $log_context, array( 'http_status' => $status ) ) ); return $decoded; } $error_code = is_array( $decoded ) && is_string( $decoded['code'] ?? null ) ? $decoded['code'] : ''; $detail = is_array( $decoded ) && is_string( $decoded['message'] ?? null ) ? $decoded['message'] : 'HTTP ' . $status; $exception = new Api_Exception( Response_Classifier::classify( $status, $error_code ), $status, $error_code, $detail ); if ( Error_Kind::PhotoRejected === $exception->kind || Error_Kind::NotFound === $exception->kind ) { // Ожидаемые отказы: фото не прошло проверку, фотосессию удалили — плагин справится сам. Logger::warning( 'aiModelka API: refused', array_merge( $log_context, $exception->log_context() ) ); } else { Logger::error( 'aiModelka API: refused', array_merge( $log_context, $exception->log_context() ) ); } throw $exception; } }