1use crate::config::BASE_URL;
2use anyhow::{anyhow, Result};
3use chrono::Local;
4use rquest::header::{HeaderMap, HeaderValue};
5use rquest::Client;
6use rquest_util::Emulation;
7use serde_json::json;
8use std::time::Duration;
9use uuid::Uuid;
10
11#[derive(Clone)]
13pub struct Session {
14 pub cookie: String,
15 pub user_agent: String,
16 pub organization_id: String,
17}
18#[derive(Clone, Debug)]
20pub struct Attachment {
21 pub file_name: String,
23 pub size: u64,
25 pub content: String,
27}
28
29pub struct Claude {
31 http: Client,
32 session: Session,
33 model: &'static str,
34}
35
36impl Claude {
37 pub fn new(session: Session, model: &'static str) -> Result<Self> {
39 let http = Client::builder()
40 .emulation(Emulation::Firefox136)
41 .timeout(Duration::from_secs(240))
42 .connect_timeout(Duration::from_secs(30))
44 .build()?;
45 Ok(Self {
46 http,
47 session,
48 model,
49 })
50 }
51
52 fn default_headers(&self) -> HeaderMap {
53 let mut headers = HeaderMap::new();
54 headers.insert("Host", HeaderValue::from_static("claude.ai"));
55 headers.insert(
56 "User-Agent",
57 HeaderValue::from_str(&self.session.user_agent).unwrap(),
58 );
59 headers
60 }
61
62 pub async fn create_chat(&self) -> Result<String> {
64 let uuid = Uuid::new_v4().to_string();
65 let url = format!(
66 "{}/api/organizations/{}/chat_conversations",
67 BASE_URL, self.session.organization_id
68 );
69 let body = json!({ "name": "", "uuid": uuid }).to_string();
70
71 let mut attempts = 0;
73 let max_attempts = 3;
74
75 while attempts < max_attempts {
76 attempts += 1;
77
78 match self
79 .http
80 .post(&url)
81 .headers(self.default_headers())
82 .header("Content-Type", "application/json")
83 .header("Cookie", &self.session.cookie)
84 .body(body.clone())
85 .send()
86 .await
87 {
88 Ok(res) => {
89 let status = res.status();
90
91 if status == 201 {
92 return Ok(uuid);
93 } else {
94 match res.text().await {
95 Ok(text) => {
96 if attempts == max_attempts {
97 return Err(anyhow!("create_chat failed: {:?}", text));
98 }
99 }
100 Err(e) => {
101 if attempts == max_attempts {
102 return Err(anyhow!(
103 "create_chat failed: unable to read response: {}",
104 e
105 ));
106 }
107 }
108 }
109 }
110 }
111 Err(e) => {
112 if attempts == max_attempts {
113 return Err(anyhow!("create_chat failed: network error: {}", e));
114 }
115 }
116 }
117
118 tokio::time::sleep(Duration::from_secs(1)).await;
120 }
121
122 Err(anyhow!(
123 "create_chat failed after {} attempts",
124 max_attempts
125 ))
126 }
127
128 pub async fn delete_chat(&self, chat_id: &str) -> Result<()> {
130 let url = format!(
131 "{}/api/organizations/{}/chat_conversations/{}",
132 BASE_URL, self.session.organization_id, chat_id
133 );
134
135 let body = format!("\"{}\"", chat_id);
136
137 let mut attempts = 0;
139 let max_attempts = 3;
140
141 while attempts < max_attempts {
142 attempts += 1;
143
144 match self
145 .http
146 .delete(&url)
147 .headers(self.default_headers())
148 .header("Content-Type", "application/json")
149 .header("Cookie", &self.session.cookie)
150 .body(body.clone())
151 .send()
152 .await
153 {
154 Ok(res) => {
155 let status = res.status();
156
157 if status == 204 {
158 return Ok(());
159 } else {
160 if attempts == max_attempts {
162 match res.text().await {
163 Ok(text) => {
164 return Err(anyhow!("delete_chat failed: {:?}", text));
165 }
166 Err(e) => {
167 return Err(anyhow!(
168 "delete_chat failed: unable to read response: {}",
169 e
170 ));
171 }
172 }
173 }
174 }
175 }
176 Err(e) => {
177 if attempts == max_attempts {
178 return Err(anyhow!("delete_chat failed: network error: {}", e));
179 }
180 }
181 }
182
183 tokio::time::sleep(Duration::from_secs(1)).await;
185 }
186
187 Err(anyhow!(
188 "delete_chat failed after {} attempts",
189 max_attempts
190 ))
191 }
192
193 pub async fn send_message(
196 &self,
197 chat_id: &str,
198 prompt: &str,
199 attachments: &[Attachment],
200 ) -> Result<String> {
201 let url = format!(
202 "{}/api/organizations/{}/chat_conversations/{}/completion",
203 BASE_URL, self.session.organization_id, chat_id
204 );
205 let mut payload = json!({
206 "attachments": [],
207 "files": [],
208 "prompt": prompt,
209 "model": self.model,
210 "timezone": Local::now().offset().to_string()
211 });
212 if !attachments.is_empty() {
213 let atts: Vec<_> = attachments
214 .iter()
215 .map(|a| {
216 json!({
217 "extracted_content": a.content,
218 "file_name": a.file_name,
219 "file_size": a.size.to_string(),
220 "file_type": "text/plain"
221 })
222 })
223 .collect();
224 payload["attachments"] = serde_json::Value::Array(atts);
225 }
226
227 let mut attempts = 0;
229 let max_attempts = 3;
230 let mut last_error = None;
231
232 while attempts < max_attempts {
233 attempts += 1;
234
235 match self
236 .http
237 .post(&url)
238 .headers(self.default_headers())
239 .header("Content-Type", "application/json")
240 .header("Accept", "text/event-stream, text/event-stream")
241 .header("Cookie", &self.session.cookie)
242 .body(payload.to_string())
243 .send()
244 .await
245 {
246 Ok(res) => {
247 let status = res.status();
248
249 if !status.is_success() {
251 let error_text = match res.text().await {
253 Ok(text) => text,
254 Err(_) => "Unable to read error response".to_string(),
255 };
256 return Err(anyhow!(
257 "API request failed with status: {} - {}",
258 status,
259 error_text
260 ));
261 }
262
263 match super::utils::decode_body(res).await {
265 Ok(bytes) => {
266 let _preview = std::str::from_utf8(&bytes[..bytes.len().min(500)])
267 .unwrap_or("Invalid UTF-8")
268 .replace('\n', "\\n");
269 let result = super::utils::parse_stream(&bytes)?;
272 return Ok(result);
273 }
274 Err(e) => {
275 eprintln!("RESPONSE DECODE ERROR: {}", e);
276 last_error = Some(e);
277 if attempts < max_attempts {
278 tokio::time::sleep(Duration::from_secs(1)).await;
280 continue;
281 }
282 }
283 }
284 }
285 Err(e) => {
286 eprintln!("NETWORK ERROR: {}", e);
287 last_error = Some(anyhow!("Network error: {}", e));
288 if attempts < max_attempts {
289 tokio::time::sleep(Duration::from_secs(1)).await;
291 continue;
292 }
293 }
294 }
295 }
296
297 Err(last_error
299 .unwrap_or_else(|| anyhow!("Failed to connect to API after {} attempts", max_attempts)))
300 }
301}