use crate::config::BASE_URL;
use anyhow::{anyhow, Result};
use chrono::Local;
use rquest::header::{HeaderMap, HeaderValue};
use rquest::Client;
use rquest_util::Emulation;
use serde_json::json;
use std::time::Duration;
use uuid::Uuid;
/// Session data for Claude API
#[derive(Clone)]
pub struct Session {
pub cookie: String,
pub user_agent: String,
pub organization_id: String,
}
/// Representation of a file attachment for send_message
#[derive(Clone, Debug)]
pub struct Attachment {
/// File name (basename)
pub file_name: String,
/// File size in bytes
pub size: u64,
/// File content as text
pub content: String,
}
/// Claude API client
pub struct Claude {
http: Client,
session: Session,
model: &'static str,
}
impl Claude {
/// Create a new Claude client
pub fn new(session: Session, model: &'static str) -> Result<Self> {
let http = Client::builder()
.emulation(Emulation::Firefox136)
.timeout(Duration::from_secs(240))
// Add retry settings to handle transient errors
.connect_timeout(Duration::from_secs(30))
.build()?;
Ok(Self {
http,
session,
model,
})
}
fn default_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("Host", HeaderValue::from_static("claude.ai"));
headers.insert(
"User-Agent",
HeaderValue::from_str(&self.session.user_agent).unwrap(),
);
headers
}
/// Create a new chat conversation
pub async fn create_chat(&self) -> Result<String> {
let uuid = Uuid::new_v4().to_string();
let url = format!(
"{}/api/organizations/{}/chat_conversations",
BASE_URL, self.session.organization_id
);
let body = json!({ "name": "", "uuid": uuid }).to_string();
// Add retry logic
let mut attempts = 0;
let max_attempts = 3;
while attempts < max_attempts {
attempts += 1;
match self
.http
.post(&url)
.headers(self.default_headers())
.header("Content-Type", "application/json")
.header("Cookie", &self.session.cookie)
.body(body.clone())
.send()
.await
{
Ok(res) => {
let status = res.status();
if status == 201 {
return Ok(uuid);
} else {
match res.text().await {
Ok(text) => {
if attempts == max_attempts {
return Err(anyhow!("create_chat failed: {:?}", text));
}
}
Err(e) => {
if attempts == max_attempts {
return Err(anyhow!(
"create_chat failed: unable to read response: {}",
e
));
}
}
}
}
}
Err(e) => {
if attempts == max_attempts {
return Err(anyhow!("create_chat failed: network error: {}", e));
}
}
}
// Wait before retrying
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(anyhow!(
"create_chat failed after {} attempts",
max_attempts
))
}
/// Delete an existing chat conversation
pub async fn delete_chat(&self, chat_id: &str) -> Result<()> {
let url = format!(
"{}/api/organizations/{}/chat_conversations/{}",
BASE_URL, self.session.organization_id, chat_id
);
let body = format!("\"{}\"", chat_id);
// Add retry logic
let mut attempts = 0;
let max_attempts = 3;
while attempts < max_attempts {
attempts += 1;
match self
.http
.delete(&url)
.headers(self.default_headers())
.header("Content-Type", "application/json")
.header("Cookie", &self.session.cookie)
.body(body.clone())
.send()
.await
{
Ok(res) => {
let status = res.status();
if status == 204 {
return Ok(());
} else {
// Only fail on final attempt
if attempts == max_attempts {
match res.text().await {
Ok(text) => {
return Err(anyhow!("delete_chat failed: {:?}", text));
}
Err(e) => {
return Err(anyhow!(
"delete_chat failed: unable to read response: {}",
e
));
}
}
}
}
}
Err(e) => {
if attempts == max_attempts {
return Err(anyhow!("delete_chat failed: network error: {}", e));
}
}
}
// Wait before retrying
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(anyhow!(
"delete_chat failed after {} attempts",
max_attempts
))
}
/// Send a message to the chat and return the completion
/// Send a message to the chat with optional attachments and return the completion
pub async fn send_message(
&self,
chat_id: &str,
prompt: &str,
attachments: &[Attachment],
) -> Result<String> {
let url = format!(
"{}/api/organizations/{}/chat_conversations/{}/completion",
BASE_URL, self.session.organization_id, chat_id
);
let mut payload = json!({
"attachments": [],
"files": [],
"prompt": prompt,
// "model": self.model,
"timezone": Local::now().offset().to_string()
});
if !attachments.is_empty() {
let atts: Vec<_> = attachments
.iter()
.map(|a| {
json!({
"extracted_content": a.content,
"file_name": a.file_name,
"file_size": a.size.to_string(),
"file_type": "text/plain"
})
})
.collect();
payload["attachments"] = serde_json::Value::Array(atts);
}
// Add retry logic for network errors
let mut attempts = 0;
let max_attempts = 3;
let mut last_error = None;
while attempts < max_attempts {
attempts += 1;
match self
.http
.post(&url)
.headers(self.default_headers())
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream, text/event-stream")
.header("Cookie", &self.session.cookie)
.body(payload.to_string())
.send()
.await
{
Ok(res) => {
let status = res.status();
// Check status before proceeding
if !status.is_success() {
// Try to get more details about the error
let error_text = match res.text().await {
Ok(text) => text,
Err(_) => "Unable to read error response".to_string(),
};
return Err(anyhow!(
"API request failed with status: {} - {}",
status,
error_text
));
}
// Process the body
match super::utils::decode_body(res).await {
Ok(bytes) => {
let _preview = std::str::from_utf8(&bytes[..bytes.len().min(500)])
.unwrap_or("Invalid UTF-8")
.replace('\n', "\\n");
// eprintln!("RESPONSE BODY: {} (first 500 bytes)", preview);
let result = super::utils::parse_stream(&bytes)?;
return Ok(result);
}
Err(e) => {
eprintln!("RESPONSE DECODE ERROR: {}", e);
last_error = Some(e);
if attempts < max_attempts {
// Wait before retrying
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
}
}
}
Err(e) => {
eprintln!("NETWORK ERROR: {}", e);
last_error = Some(anyhow!("Network error: {}", e));
if attempts < max_attempts {
// Wait before retrying
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
}
}
}
// All attempts failed
Err(last_error
.unwrap_or_else(|| anyhow!("Failed to connect to API after {} attempts", max_attempts)))
}
}