use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::json;
use crate::error::Error;
use crate::types::*;
#[derive(Debug, Clone)]
pub struct Client {
http: reqwest::Client,
base_url: String,
}
#[derive(Deserialize)]
struct Envelope {
data: serde_json::Value,
}
impl Client {
pub fn new(api_key: &str) -> Self {
Self::with_base_url(api_key, "https://api.meshbrow.dev")
}
pub fn with_base_url(api_key: &str, base_url: &str) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", api_key)).unwrap(),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(USER_AGENT, HeaderValue::from_static("meshbrow-rust/0.1.0"));
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap();
Self {
http,
base_url: base_url.to_string(),
}
}
async fn request<T: DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http.request(method, &url);
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send().await?;
let status = resp.status();
if status.as_u16() >= 400 {
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
body,
});
}
let text = resp.text().await?;
if text.is_empty() {
return Err(Error::Api {
status: 204,
body: "empty response".into(),
});
}
if let Ok(envelope) = serde_json::from_str::<Envelope>(&text) {
if !envelope.data.is_null() {
return Ok(serde_json::from_value(envelope.data)?);
}
}
Ok(serde_json::from_str(&text)?)
}
async fn request_empty(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<(), Error> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http.request(method, &url);
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send().await?;
let status = resp.status();
if status.as_u16() >= 400 {
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
body,
});
}
Ok(())
}
pub async fn create_session(
&self,
params: Option<CreateSessionParams>,
) -> Result<Session, Error> {
let body = match params {
Some(p) => serde_json::to_value(p).unwrap_or(json!({"stealth": "max"})),
None => json!({"stealth": "max"}),
};
self.request(reqwest::Method::POST, "/v1/sessions", Some(body))
.await
}
pub async fn get_session(&self, id: &str) -> Result<Session, Error> {
self.request(reqwest::Method::GET, &format!("/v1/sessions/{}", id), None)
.await
}
pub async fn list_sessions(&self) -> Result<SessionListResponse, Error> {
self.request(reqwest::Method::GET, "/v1/sessions", None)
.await
}
pub async fn destroy_session(&self, id: &str, save_profile: bool) -> Result<(), Error> {
let body = if save_profile {
Some(json!({"save_profile": true}))
} else {
None
};
self.request_empty(
reqwest::Method::DELETE,
&format!("/v1/sessions/{}", id),
body,
)
.await
}
pub async fn navigate(
&self,
session_id: &str,
url: &str,
wait_until: Option<&str>,
) -> Result<NavigateResponse, Error> {
self.request(
reqwest::Method::POST,
&format!("/v1/sessions/{}/navigate", session_id),
Some(json!({
"url": url,
"wait_until": wait_until.unwrap_or("load"),
})),
)
.await
}
pub async fn screenshot(
&self,
session_id: &str,
selector: Option<&str>,
full_page: bool,
) -> Result<Screenshot, Error> {
let mut body = json!({"full_page": full_page});
if let Some(sel) = selector {
body["selector"] = json!(sel);
}
self.request(
reqwest::Method::POST,
&format!("/v1/sessions/{}/screenshot", session_id),
Some(body),
)
.await
}
pub async fn click(&self, session_id: &str, selector: &str) -> Result<(), Error> {
self.request_empty(
reqwest::Method::POST,
&format!("/v1/sessions/{}/click", session_id),
Some(json!({"selector": selector})),
)
.await
}
pub async fn type_text(
&self,
session_id: &str,
selector: &str,
text: &str,
clear: bool,
) -> Result<(), Error> {
self.request_empty(
reqwest::Method::POST,
&format!("/v1/sessions/{}/type", session_id),
Some(json!({"selector": selector, "text": text, "clear": clear})),
)
.await
}
pub async fn extract(
&self,
session_id: &str,
selector: Option<&str>,
max_length: Option<u32>,
) -> Result<ExtractResponse, Error> {
let mut body = json!({});
if let Some(sel) = selector {
body["selector"] = json!(sel);
}
if let Some(ml) = max_length {
body["max_length"] = json!(ml);
}
self.request(
reqwest::Method::POST,
&format!("/v1/sessions/{}/extract", session_id),
Some(body),
)
.await
}
pub async fn execute(&self, session_id: &str, script: &str) -> Result<ExecuteResponse, Error> {
self.request(
reqwest::Method::POST,
&format!("/v1/sessions/{}/execute", session_id),
Some(json!({"script": script})),
)
.await
}
pub async fn create_fleet(&self, params: CreateFleetParams) -> Result<Fleet, Error> {
let body = serde_json::to_value(params).unwrap();
self.request(reqwest::Method::POST, "/v1/fleet", Some(body))
.await
}
pub async fn get_fleet(&self, id: &str) -> Result<Fleet, Error> {
self.request(reqwest::Method::GET, &format!("/v1/fleet/{}", id), None)
.await
}
pub async fn destroy_fleet(&self, id: &str) -> Result<(), Error> {
self.request_empty(reqwest::Method::DELETE, &format!("/v1/fleet/{}", id), None)
.await
}
}