token = $token; $this->account_id = (int) $account_id; } /** * Builds a client from the saved settings. * * @return ConvoHub_API */ public static function from_settings() { $settings = ConvoHub_Settings::get(); return new self( $settings['api_token'], $settings['account_id'] ); } /** * The signed-in user and every account the token can access. * * @return array|WP_Error */ public function profile() { return $this->request( 'GET', '/api/v1/profile' ); } /** * The configured account. * * @return array|WP_Error */ public function account() { return $this->account_request( 'GET', '' ); } /** * All inboxes of the account. * * @return array|WP_Error List of inboxes. */ public function inboxes() { $data = $this->account_request( 'GET', '/inboxes' ); return is_wp_error( $data ) ? $data : (array) ( $data['payload'] ?? array() ); } /** * Creates a website (web widget) inbox. * * @param string $name Inbox name. * @param string $website_url Site URL. * @return array|WP_Error The created inbox. */ public function create_web_widget_inbox( $name, $website_url ) { return $this->account_request( 'POST', '/inboxes', array( 'name' => $name, 'channel' => array( 'type' => 'web_widget', 'website_url' => $website_url, ), ) ); } /** * Creates an API-channel inbox (used to receive website leads). * * @param string $name Inbox name. * @return array|WP_Error The created inbox. */ public function create_api_inbox( $name ) { return $this->account_request( 'POST', '/inboxes', array( 'name' => $name, 'channel' => array( 'type' => 'api' ), ) ); } /** * Creates (or finds) the contact and opens a conversation with the lead's message in one call. * ConvoHub matches an existing contact by identifier, then email, then phone number. * * @param int $inbox_id API-channel inbox ID. * @param array $contact Keys: name, email, phone_number. * @param string $content First message, sent as the contact. * @param array $custom_attributes Conversation custom attributes. * @return array|WP_Error The created conversation. */ public function create_lead_conversation( $inbox_id, array $contact, $content, array $custom_attributes ) { return $this->account_request( 'POST', '/conversations', array( 'inbox_id' => (int) $inbox_id, 'contact' => $contact, 'message' => array( 'content' => $content, 'message_type' => 'incoming', ), 'custom_attributes' => $custom_attributes, ) ); } /** * Updates channel attributes of an inbox (for example `allowed_domains`). * * @param int $inbox_id Inbox ID. * @param array $channel Channel attributes. * @return array|WP_Error The updated inbox. */ public function update_inbox_channel( $inbox_id, array $channel ) { return $this->account_request( 'PATCH', '/inboxes/' . absint( $inbox_id ), array( 'channel' => $channel ) ); } /** * Sends an email through ConvoHub as a new conversation of an email inbox. * * ConvoHub takes the subject of an email from the conversation (`additional_attributes.mail_subject`), reads * `content` as plain text and sends `email_html_content` as the HTML part. The Developer API has no fields for * either, so an email is created here instead: the contact is found or created in the same request. Each call is * a new conversation with its own subject, unless the inbox is locked to a single conversation per contact, in * which case ConvoHub keeps the first subject and adds "Re:". * * @param int $inbox_id Email inbox ID. * @param array $contact Keys: email, name, identifier, custom_attributes. * @param string $subject Subject, already rendered, plain text. * @param string $text Plain-text version of the email. Never empty: ConvoHub ignores the HTML of an empty message. * @param string $html The HTML body, as it is to be delivered. * @param array $options `to_emails`, `cc_emails`, `bcc_emails` (comma-separated). * @return array|WP_Error `conversation_id` (the number shown in ConvoHub) and `message_id` (0 when ConvoHub did not list the message). */ public function create_email_conversation( $inbox_id, array $contact, $subject, $text, $html, array $options = array() ) { $message = array_merge( array( 'content' => $text, 'email_html_content' => $html, 'message_type' => 'outgoing', 'private' => false, ), array_intersect_key( $options, array_flip( array( 'to_emails', 'cc_emails', 'bcc_emails' ) ) ) ); $data = $this->account_request( 'POST', '/conversations', array( 'inbox_id' => (int) $inbox_id, 'contact' => $contact, 'additional_attributes' => array( 'mail_subject' => $subject ), 'message' => $message, ) ); if ( is_wp_error( $data ) ) { return $data; } return array( 'conversation_id' => (int) ( $data['id'] ?? 0 ), 'message_id' => (int) ( $data['messages'][0]['id'] ?? 0 ), ); } /** * Sends a WhatsApp template or an email to a contact through the Developer API. * * The payload is `{inbox_id, type: 'whatsapp_template' | 'text', contact: {...}, message: {...}}`. For an email use * create_email_conversation(): this API cannot set a subject or HTML. * Each call creates a conversation unless the inbox is locked to a single conversation per contact. * ConvoHub answers a payload it cannot use with a generic 422. * * @param array $payload Message payload. * @return array|WP_Error `message_id` and `conversation_id` of the created message. */ public function send_message( array $payload ) { if ( $this->account_id <= 0 ) { return new WP_Error( 'convohub_no_account', __( 'Choose a ConvoHub account first.', 'convohub' ) ); } return $this->request( 'POST', '/convohub/developer/api/v1/messages', $payload, array( 'api_account_id' => (string) $this->account_id ) ); } /** * The most recent messages of a conversation, including each message's delivery `status`. * * @param int $conversation_id Conversation ID (the `conversation_id` returned when the message was sent). * @return array|WP_Error List of messages. */ public function conversation_messages( $conversation_id ) { $data = $this->account_request( 'GET', '/conversations/' . absint( $conversation_id ) . '/messages' ); return is_wp_error( $data ) ? $data : (array) ( $data['payload'] ?? array() ); } /** * Searches contacts. ConvoHub matches the text anywhere in the name, email, phone number or identifier, so the * caller must pick the exact contact from the result. * * @param string $query Search text. * @return array|WP_Error List of contacts (a first page only). */ public function search_contacts( $query ) { $data = $this->account_request( 'GET', '/contacts/search?q=' . rawurlencode( $query ) ); return is_wp_error( $data ) ? $data : (array) ( $data['payload'] ?? array() ); } /** * Merges custom attributes into a contact. Attributes that are not sent are kept. * * @param int $contact_id Contact ID. * @param array $custom_attributes Attribute name => value. * @return array|WP_Error */ public function update_contact_attributes( $contact_id, array $custom_attributes ) { return $this->account_request( 'PATCH', '/contacts/' . absint( $contact_id ), array( 'custom_attributes' => $custom_attributes ) ); } /** * Connects an integration app to the account (for example `woocommerce`). * * @param string $app_id App ID. * @param array $settings App settings. * @return array|WP_Error The created hook. */ public function create_integration_hook( $app_id, array $settings ) { return $this->account_request( 'POST', '/integrations/hooks', array( 'hook' => array( 'app_id' => $app_id, 'settings' => $settings, ), ) ); } /** * Disconnects an integration. * * @param int $hook_id Hook ID. * @return array|WP_Error */ public function delete_integration_hook( $hook_id ) { return $this->account_request( 'DELETE', '/integrations/hooks/' . absint( $hook_id ) ); } /** * Webhooks registered on the account. * * @return array|WP_Error List of webhooks (id, url, subscriptions, secret). */ public function webhooks() { $data = $this->account_request( 'GET', '/webhooks' ); return is_wp_error( $data ) ? $data : (array) ( $data['payload']['webhooks'] ?? array() ); } /** * Registers an account webhook. * * @param string $url Public URL ConvoHub posts events to. * @param string $name Webhook name. * @param string[] $events Event names to subscribe to. * @return array|WP_Error The created webhook, including its signing secret. */ public function create_webhook( $url, $name, array $events ) { $data = $this->account_request( 'POST', '/webhooks', array( 'webhook' => array( 'url' => $url, 'name' => $name, 'subscriptions' => $events, ), ) ); return is_wp_error( $data ) ? $data : (array) ( $data['payload']['webhook'] ?? array() ); } /** * Changes the events a webhook is subscribed to. ConvoHub does not allow changing its URL. * * @param int $webhook_id Webhook ID. * @param string[] $events Event names. * @return array|WP_Error The updated webhook. */ public function update_webhook_events( $webhook_id, array $events ) { $data = $this->account_request( 'PATCH', '/webhooks/' . absint( $webhook_id ), array( 'webhook' => array( 'subscriptions' => $events ) ) ); return is_wp_error( $data ) ? $data : (array) ( $data['payload']['webhook'] ?? array() ); } /** * Deletes a webhook. * * @param int $webhook_id Webhook ID. * @return array|WP_Error */ public function delete_webhook( $webhook_id ) { return $this->account_request( 'DELETE', '/webhooks/' . absint( $webhook_id ) ); } /** * Synced message templates of an inbox. * * @param int $inbox_id Inbox ID. * @return array|WP_Error List of templates. */ public function message_templates( $inbox_id ) { $data = $this->account_request( 'GET', '/inboxes/' . absint( $inbox_id ) . '/message_templates' ); return is_wp_error( $data ) ? $data : (array) ( $data['payload'] ?? array() ); } /** * Performs an account-scoped request. * * @param string $method HTTP method. * @param string $suffix Path after `/api/v1/accounts/:id`. * @param array|null $body JSON body. * @return array|WP_Error */ private function account_request( $method, $suffix, $body = null ) { if ( $this->account_id <= 0 ) { return new WP_Error( 'convohub_no_account', __( 'Choose a ConvoHub account first.', 'convohub' ) ); } return $this->request( $method, '/api/v1/accounts/' . $this->account_id . $suffix, $body ); } /** * Performs a request and normalizes failures into a WP_Error. * * @param string $method HTTP method. * @param string $path Path starting with a slash. * @param array|null $body JSON body. * @param array $headers Extra request headers. * @return array|WP_Error Decoded JSON body (empty array for an empty body). */ public function request( $method, $path, $body = null, array $headers = array() ) { if ( '' === $this->token ) { return new WP_Error( 'convohub_not_connected', __( 'ConvoHub is not connected.', 'convohub' ) ); } $args = array( 'method' => $method, 'timeout' => 15, 'redirection' => 0, 'headers' => array_merge( array( 'api_access_token' => $this->token, 'Accept' => 'application/json', 'Content-Type' => 'application/json', ), $headers ), ); if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( ConvoHub_Settings::BASE_URL . $path, $args ); if ( is_wp_error( $response ) ) { return new WP_Error( 'convohub_unreachable', /* translators: %s: error message from the HTTP layer. */ sprintf( __( 'Could not reach ConvoHub: %s', 'convohub' ), $response->get_error_message() ) ); } $status = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status < 200 || $status >= 300 ) { return self::error_from_response( $status, $data ); } return is_array( $data ) ? $data : array(); } /** * Maps ConvoHub's error shapes (`{error}` or `{message, attributes}`) to one WP_Error. * * @param int $status HTTP status. * @param mixed $data Decoded body. * @return WP_Error */ public static function error_from_response( $status, $data ) { $detail = ''; if ( is_array( $data ) ) { $detail = (string) ( $data['error'] ?? $data['message'] ?? '' ); } switch ( $status ) { case 401: $code = 'convohub_unauthorized'; $message = __( 'ConvoHub no longer accepts this site\'s access. Disconnect, then connect again with a new key.', 'convohub' ); break; case 402: $code = 'convohub_billing'; $message = __( 'The ConvoHub account is in read-only billing mode or has hit a plan limit.', 'convohub' ); break; case 403: $code = 'convohub_api_disabled'; $message = __( 'API access is not enabled for this ConvoHub account.', 'convohub' ); break; case 404: $code = 'convohub_not_found'; $message = __( 'ConvoHub could not find that resource.', 'convohub' ); break; case 422: $code = 'convohub_invalid'; $message = '' !== $detail ? $detail : __( 'ConvoHub rejected the request.', 'convohub' ); break; case 429: $code = 'convohub_rate_limited'; $message = __( 'ConvoHub is rate limiting requests. Try again shortly.', 'convohub' ); break; default: $code = $status >= 500 ? 'convohub_server_error' : 'convohub_error'; /* translators: %d: HTTP status code. */ $message = sprintf( __( 'ConvoHub returned an unexpected response (HTTP %d).', 'convohub' ), $status ); }//end switch return new WP_Error( $code, $message, array( 'status' => $status, 'detail' => $detail, ) ); } }