*/ private $settings; /** * Constructor. * * @param array $settings Settings. */ public function __construct( array $settings ) { $this->settings = $settings; } /** * Get account balance. */ public function get_balance(): ApiResult { return $this->request( 'GET', '/balance/' ); } /** * Send SMS. * * @param string $message Message text. * @param string[] $phones Recipient phones. * @param array $options Extra options. */ public function send_sms( string $message, array $phones, array $options = array() ): ApiResult { $body = array( 'from' => $this->get_sms_sender(), 'to' => array_values( $phones ), 'message' => $message, ); if ( isset( $options['priority'] ) ) { $body['priority'] = absint( $options['priority'] ); } if ( isset( $options['ttl'] ) ) { $body['ttl'] = absint( $options['ttl'] ); } return $this->request( 'POST', '/message/send/', $body ); } /** * Send Viber. * * @param string $message Message text. * @param string[] $phones Recipient phones. * @param array $options Extra options. */ public function send_viber( string $message, array $phones, array $options = array() ): ApiResult { $body = array( 'im_sender' => $this->get_viber_sender(), 'to' => array_values( $phones ), 'im_message' => $message, 'im_ttl' => isset( $options['ttl'] ) ? absint( $options['ttl'] ) : absint( $this->settings['viber_ttl'] ?? 600 ), ); return $this->request( 'POST', '/im/send/', $body ); } /** * Get SMS sender from settings. */ private function get_sms_sender(): string { $sender = isset( $this->settings['sms_sender'] ) ? trim( (string) $this->settings['sms_sender'] ) : ''; return '' === $sender ? 'woocom' : $sender; } /** * Get Viber sender from settings. */ private function get_viber_sender(): string { $sender = isset( $this->settings['viber_sender'] ) ? trim( (string) $this->settings['viber_sender'] ) : ''; return '' === $sender ? 'woocomvib' : $sender; } /** * Get SMS statuses. * * @param string[] $message_ids Message IDs. */ public function get_sms_statuses( array $message_ids ): ApiResult { return $this->get_statuses( '/message/status/', $message_ids ); } /** * Get Viber statuses. * * @param string[] $message_ids Message IDs. */ public function get_viber_statuses( array $message_ids ): ApiResult { return $this->get_statuses( '/im/status/', $message_ids ); } /** * Get message statuses one by one using documented GET query parameters. * * @param string $path API path. * @param string[] $message_ids Message IDs. */ private function get_statuses( string $path, array $message_ids ): ApiResult { $statuses = array(); $raw = array(); foreach ( array_values( array_filter( $message_ids ) ) as $message_id ) { $result = $this->request( 'GET', $path, array( 'message_id' => (string) $message_id, 'timezone' => wp_timezone_string(), ) ); $raw[ (string) $message_id ] = $result->get_raw(); if ( ! $result->is_success() ) { return ApiResult::failure( $result->get_error(), $result->get_status_code(), $raw ); } $data = $result->get_data(); if ( isset( $data['statuses'] ) && is_array( $data['statuses'] ) ) { $statuses[ (string) $message_id ] = $this->normalize_status_items( $data['statuses'], (string) $message_id ); } } return ApiResult::success( array( 'statuses' => $statuses ), 200, $raw ); } /** * Execute API request. * * @param string $method HTTP method. * @param string $path API path. * @param array $body Request body. */ private function request( string $method, string $path, array $body = array() ): ApiResult { $api_key = (string) ( $this->settings['api_key'] ?? '' ); if ( '' === $api_key ) { return ApiResult::failure( __( 'API key is required.', 'inteltele' ) ); } $query_args = array( 'api_key' => $api_key ); if ( 'POST' !== strtoupper( $method ) && ! empty( $body ) ) { $query_args = array_merge( $query_args, $body ); } $url = add_query_arg( $query_args, self::BASE_URL . $path ); $args = array( 'timeout' => 20, 'headers' => array( 'Accept' => 'application/json', ), ); if ( 'POST' === strtoupper( $method ) ) { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( $body ); $response = wp_remote_post( $url, $args ); } else { $response = wp_remote_get( $url, $args ); } if ( is_wp_error( $response ) ) { return ApiResult::failure( $response->get_error_message(), 0, $response ); } $status_code = (int) wp_remote_retrieve_response_code( $response ); $raw_body = (string) wp_remote_retrieve_body( $response ); $decoded = json_decode( $raw_body, true ); if ( null === $decoded && '' !== $raw_body ) { return ApiResult::failure( __( 'API returned an invalid JSON response.', 'inteltele' ), $status_code, $raw_body ); } if ( $status_code < 200 || $status_code >= 300 || $this->is_error_response( $decoded ) ) { return ApiResult::failure( $this->normalize_error( $decoded, $status_code ), $status_code, $decoded ); } return ApiResult::success( $this->normalize_data( $path, is_array( $decoded ) ? $decoded : array() ), $status_code, $decoded ); } /** * Normalize data by endpoint. * * @param string $path Path. * @param array $decoded Decoded response. * @return array */ private function normalize_data( string $path, array $decoded ): array { if ( '/balance/' === $path ) { return array( 'balance' => isset( $decoded['balance'] ) ? (string) $decoded['balance'] : '', ); } if ( in_array( $path, array( '/message/send/', '/im/send/' ), true ) ) { return array( 'items' => $this->normalize_send_items( $decoded ), ); } if ( in_array( $path, array( '/message/status/', '/im/status/' ), true ) ) { return array( 'statuses' => $decoded, ); } return $decoded; } /** * Normalize send endpoint response items. * * @param array $decoded Decoded response. * @return array> */ private function normalize_send_items( array $decoded ): array { $items = array(); if ( isset( $decoded['reply'] ) && is_array( $decoded['reply'] ) ) { $source = $decoded['reply']; } elseif ( isset( $decoded['items'] ) && is_array( $decoded['items'] ) ) { $source = $decoded['items']; } else { $source = $decoded; } foreach ( $source as $item ) { if ( is_array( $item ) && isset( $item['status'] ) ) { $items[] = $item; } } return $items; } /** * Normalize one status endpoint response to a list of status items. * * @param array $decoded Decoded response. * @param string $message_id Message ID. * @return array> */ private function normalize_status_items( array $decoded, string $message_id ): array { if ( isset( $decoded[ $message_id ] ) && is_array( $decoded[ $message_id ] ) ) { return $this->status_item_list( $decoded[ $message_id ] ); } if ( isset( $decoded['reply'] ) && is_array( $decoded['reply'] ) ) { return $this->status_item_list( $decoded['reply'] ); } if ( isset( $decoded['status'] ) ) { return array( array( 'status' => (string) $decoded['status'], 'date' => isset( $decoded['date'] ) ? (string) $decoded['date'] : '', ), ); } return $this->status_item_list( $decoded ); } /** * Convert API status items to a stable list. * * @param array $items Items. * @return array> */ private function status_item_list( array $items ): array { $list = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) || ! isset( $item['status'] ) ) { continue; } $list[] = array( 'status' => (string) $item['status'], 'date' => isset( $item['date'] ) ? (string) $item['date'] : '', ); } return $list; } /** * Check API error payloads that can be returned with HTTP 200. * * @param mixed $decoded Decoded response. */ private function is_error_response( $decoded ): bool { if ( ! is_array( $decoded ) ) { return false; } if ( ! empty( $decoded['error'] ) ) { return true; } if ( isset( $decoded['status'] ) && is_string( $decoded['status'] ) ) { return 1 === preg_match( '/^error:/i', trim( $decoded['status'] ) ); } return false; } /** * Normalize API error message. * * @param mixed $decoded Decoded response. * @param int $status_code HTTP status code. */ private function normalize_error( $decoded, int $status_code ): string { $status = ''; if ( is_array( $decoded ) && isset( $decoded['status'] ) ) { $status = (string) $decoded['status']; $status = preg_replace( '/^error:\s*/i', '', $status ); $status = trim( (string) $status ); } if ( '' === $status ) { return sprintf( /* translators: %d: HTTP status code. */ __( 'API request failed with HTTP status %d.', 'inteltele' ), $status_code ); } return self::translate_api_status( $status ); } /** * Translate known API status messages. * * @param string $status API status. */ public static function translate_api_status( string $status ): string { $map = array( 'Low balance' => __( 'Insufficient balance.', 'inteltele' ), 'Repeat detected' => __( 'Duplicate message detected by API.', 'inteltele' ), 'Message not saved. Please, repeat it' => __( 'Message was not saved by API. Please try again.', 'inteltele' ), 'Message contains stopwords and not send' => __( 'Message contains stop words and was not sent.', 'inteltele' ), 'Message contains stoplink and not send' => __( 'Message contains a blocked link and was not sent.', 'inteltele' ), "Route can't handle the message" => __( 'API route cannot handle this message.', 'inteltele' ), 'No route to country' => __( 'No route to recipient country.', 'inteltele' ), 'Not found client' => __( 'API client was not found.', 'inteltele' ), 'Not found account' => __( 'API account was not found.', 'inteltele' ), "Can't determine country" => __( 'Could not determine recipient country.', 'inteltele' ), 'Message text is empty' => __( 'Message text is empty.', 'inteltele' ), 'Recipient is empty' => __( 'Recipient phone is empty.', 'inteltele' ), 'Sender is not valid' => __( 'Sender name is not valid.', 'inteltele' ), 'Sender is empty' => __( 'Sender name is empty.', 'inteltele' ), 'Sms message is too long' => __( 'SMS message is too long.', 'inteltele' ), 'Wrong Sms source' => __( 'SMS fallback sender is invalid.', 'inteltele' ), 'Invalid auth' => __( 'API key is invalid.', 'inteltele' ), 'You are banned' => __( 'API account is banned.', 'inteltele' ), 'Too many requests' => __( 'Too many API requests. Please try again later.', 'inteltele' ), 'Not have messageID in request' => __( 'Message ID is missing in status request.', 'inteltele' ), 'Some error' => __( 'API returned an unspecified error.', 'inteltele' ), ); return $map[ $status ] ?? $status; } }