highlevel-api 0.1.0

Unofficial Rust SDK for the GoHighLevel (HighLevel) API
Documentation
use reqwest::Client;
use std::sync::Arc;
use url::Url;
use crate::{
    config::HighLevelConfig,
    error::{Error, Result},
};
use super::token::TokenData;

const AUTH_BASE_URL: &str = "https://marketplace.gohighlevel.com";
const TOKEN_URL: &str = "https://services.leadconnectorhq.com/oauth/token";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessType {
    Online,
    Offline,
}

impl AccessType {
    pub fn as_str(self) -> &'static str {
        match self {
            AccessType::Online => "online",
            AccessType::Offline => "offline",
        }
    }
}

pub struct OAuthClient {
    config: Arc<HighLevelConfig>,
    inner: Client,
}

impl OAuthClient {
    pub fn new(config: Arc<HighLevelConfig>, inner: Client) -> Self {
        Self { config, inner }
    }

    /// Build the authorization URL to redirect the user to for consent.
    pub fn authorization_url(&self, scopes: &[&str], access_type: AccessType) -> String {
        let mut url = Url::parse(AUTH_BASE_URL).expect("static URL is valid");
        url.set_path("/oauth/chooselocation");
        {
            let mut q = url.query_pairs_mut();
            q.append_pair("response_type", "code");
            if let Some(redirect_uri) = &self.config.redirect_uri {
                q.append_pair("redirect_uri", redirect_uri);
            }
            q.append_pair("client_id", &self.config.client_id);
            q.append_pair("scope", &scopes.join(" "));
            q.append_pair("access_type", access_type.as_str());
        }
        url.to_string()
    }

    /// Exchange an authorization code for an access + refresh token pair.
    pub async fn exchange_code(&self, code: &str) -> Result<TokenData> {
        let redirect_uri = self.config.redirect_uri.clone().unwrap_or_default();
        let params = [
            ("client_id", self.config.client_id.as_str()),
            ("client_secret", self.config.client_secret.as_str()),
            ("grant_type", "authorization_code"),
            ("code", code),
            ("redirect_uri", &redirect_uri),
        ];
        self.fetch_token(&params).await
    }

    /// Use a refresh token to obtain a new access token.
    pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenData> {
        let params = [
            ("client_id", self.config.client_id.as_str()),
            ("client_secret", self.config.client_secret.as_str()),
            ("grant_type", "refresh_token"),
            ("refresh_token", refresh_token),
        ];
        self.fetch_token(&params).await
    }

    async fn fetch_token(&self, params: &[(&str, &str)]) -> Result<TokenData> {
        let resp = self.inner.post(TOKEN_URL).form(params).send().await?;
        if !resp.status().is_success() {
            let status = resp.status().as_u16();
            let message = resp.text().await.unwrap_or_default();
            return Err(Error::Api { status, message });
        }
        let mut token: TokenData = resp.json().await?;
        token.mark_issued_now();
        Ok(token)
    }
}