use base64::Engine;
use reqwest::Client;
use serde::de::DeserializeOwned;
use crate::config::Config;
#[derive(Clone)]
pub struct ConfluenceClient {
config: Config,
http: Client,
}
impl ConfluenceClient {
pub fn new(config: Config) -> Self {
let http = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.expect("failed to build HTTP client");
Self { config, http }
}
pub fn config(&self) -> &Config {
&self.config
}
fn auth_header(&self) -> String {
if self.config.use_bearer {
format!("Bearer {}", self.config.api_token)
} else {
let credentials = format!(
"{}:{}",
self.config.email.as_deref().unwrap_or(""),
self.config.api_token
);
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
format!("Basic {encoded}")
}
}
fn base_url(&self) -> String {
if self.config.is_cloud {
format!("{}/wiki/api/v2", self.config.host)
} else {
format!("{}/rest/api", self.config.host)
}
}
pub async fn request<T: DeserializeOwned>(
&self,
endpoint: &str,
method: &str,
body: Option<&serde_json::Value>,
use_v1_api: bool,
) -> Result<T, String> {
let url = if use_v1_api && self.config.is_cloud {
format!("{}/wiki/rest/api{endpoint}", self.config.host)
} else {
format!("{}{endpoint}", self.base_url())
};
eprintln!("[DEBUG] Fetching: {url}");
eprintln!(
"[DEBUG] Auth type: {}",
if self.config.use_bearer {
"Bearer"
} else {
"Basic"
}
);
let mut req = match method {
"POST" => self.http.post(&url),
"PUT" => self.http.put(&url),
"DELETE" => self.http.delete(&url),
_ => self.http.get(&url),
};
req = req
.header("Authorization", self.auth_header())
.header("Accept", "application/json")
.header("Content-Type", "application/json");
if let Some(b) = body {
req = req.json(b);
}
let resp = req.send().await.map_err(|e| format!("HTTP error: {e}"))?;
let status = resp.status();
if status == reqwest::StatusCode::NO_CONTENT {
return serde_json::from_str("{}").map_err(|e| e.to_string());
}
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
return Err(format!("Confluence API error ({status}): {text}"));
}
resp.json::<T>()
.await
.map_err(|e| format!("JSON parse error: {e}"))
}
pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, String> {
self.request(endpoint, "GET", None, false).await
}
pub async fn get_v1<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, String> {
self.request(endpoint, "GET", None, true).await
}
pub async fn post<T: DeserializeOwned>(
&self,
endpoint: &str,
body: &serde_json::Value,
) -> Result<T, String> {
self.request(endpoint, "POST", Some(body), false).await
}
pub async fn put<T: DeserializeOwned>(
&self,
endpoint: &str,
body: &serde_json::Value,
) -> Result<T, String> {
self.request(endpoint, "PUT", Some(body), false).await
}
pub async fn delete(&self, endpoint: &str) -> Result<(), String> {
let _: serde_json::Value = self.request(endpoint, "DELETE", None, false).await?;
Ok(())
}
pub async fn fetch_binary(&self, download_url: &str) -> Result<(String, String, usize), String> {
let url = if download_url.starts_with('/') {
format!("{}{download_url}", self.config.host)
} else {
download_url.to_string()
};
eprintln!("[DEBUG] Fetching binary: {url}");
let resp = self
.http
.get(&url)
.header("Authorization", self.auth_header())
.send()
.await
.map_err(|e| format!("HTTP error: {e}"))?;
if !resp.status().is_success() {
return Err(format!(
"Failed to fetch attachment ({}): {}",
resp.status(),
resp.status().canonical_reason().unwrap_or("unknown")
));
}
let mime = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
let bytes = resp.bytes().await.map_err(|e| format!("Read error: {e}"))?;
let size = bytes.len();
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
Ok((b64, mime, size))
}
}