1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4use ureq;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum PasteVisibility {
10 Public,
12 Unlisted,
14 Private,
16}
17
18impl Default for PasteVisibility {
19 fn default() -> Self {
20 PasteVisibility::Unlisted
21 }
22}
23
24impl std::str::FromStr for PasteVisibility {
25 type Err = String;
26 fn from_str(s: &str) -> Result<Self, Self::Err> {
27 match s.to_lowercase().as_str() {
28 "public" => Ok(PasteVisibility::Public),
29 "unlisted" => Ok(PasteVisibility::Unlisted),
30 "private" => Ok(PasteVisibility::Private),
31 _ => Err(format!("Invalid visibility: {}", s)),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum PasteExpiration {
40 #[serde(rename = "10m")]
42 Min10,
43 #[serde(rename = "1h")]
45 Hour1,
46 #[serde(rename = "1d")]
48 Day1,
49 #[serde(rename = "1w")]
51 Week1,
52 #[serde(rename = "2w")]
54 Week2,
55 #[serde(rename = "1m")]
57 Month1,
58 #[serde(rename = "6m")]
60 Month6,
61 #[serde(rename = "1y")]
63 Year1,
64 Never,
66}
67
68impl Default for PasteExpiration {
69 fn default() -> Self {
70 PasteExpiration::Week1
71 }
72}
73
74impl std::str::FromStr for PasteExpiration {
75 type Err = String;
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 match s.to_lowercase().as_str() {
78 "10m" => Ok(PasteExpiration::Min10),
79 "1h" => Ok(PasteExpiration::Hour1),
80 "1d" => Ok(PasteExpiration::Day1),
81 "1w" => Ok(PasteExpiration::Week1),
82 "2w" => Ok(PasteExpiration::Week2),
83 "1m" => Ok(PasteExpiration::Month1),
84 "6m" => Ok(PasteExpiration::Month6),
85 "1y" => Ok(PasteExpiration::Year1),
86 "never" => Ok(PasteExpiration::Never),
87 _ => Err(format!("Invalid expiration: {}", s)),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "lowercase")]
95pub enum PasteCategory {
96 Code,
98 Logs,
100 Configuration,
102 Data,
104 Documentation,
106 Other,
108}
109
110impl Default for PasteCategory {
111 fn default() -> Self {
112 PasteCategory::Code
113 }
114}
115
116impl std::str::FromStr for PasteCategory {
117 type Err = String;
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 match s.to_lowercase().as_str() {
120 "code" => Ok(PasteCategory::Code),
121 "logs" => Ok(PasteCategory::Logs),
122 "configuration" => Ok(PasteCategory::Configuration),
123 "data" => Ok(PasteCategory::Data),
124 "documentation" => Ok(PasteCategory::Documentation),
125 "other" => Ok(PasteCategory::Other),
126 _ => Err(format!("Invalid category: {}", s)),
127 }
128 }
129}
130
131#[derive(Debug, Serialize, Clone)]
133pub struct CreatePasteRequest {
134 pub content: String,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub title: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub language: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub visibility: Option<PasteVisibility>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub expiration: Option<PasteExpiration>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub category: Option<PasteCategory>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub tags: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub max_views: Option<u32>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub password: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub burn_after_read: Option<bool>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub folder_id: Option<u64>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub encrypted: Option<bool>,
169}
170
171#[derive(Debug, Deserialize)]
173pub struct PasteCreated {
174 pub id: String,
176 pub url: String,
178 pub raw_url: String,
180 pub delete_token: String,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub expires_at: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub max_views: Option<u32>,
188}
189
190#[derive(Debug, Deserialize)]
192pub struct Paste {
193 pub id: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub title: Option<String>,
198 pub content: String,
200 pub language: String,
202 pub visibility: String,
204 pub views: u32,
206 pub created_at: String,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub expires_at: Option<String>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub max_views: Option<u32>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub updated_at: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub url: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub category: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub tags: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub burn_after_read: Option<bool>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub encrypted: Option<bool>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub folder_id: Option<u64>,
235}
236
237#[derive(Debug, Deserialize)]
238struct ErrorResponse {
239 error: String,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 _retry_after: Option<u64>,
242}
243
244const BASE_URL: &str = "https://pastebin.ai/api/v1";
245
246pub struct PastebinClient {
252 agent: ureq::Agent,
253 auth_token: Option<String>,
254 delete_token: Option<String>,
255 delete_paste_id: Option<String>,
256}
257
258impl PastebinClient {
259 pub fn new() -> Result<Self> {
261 let agent = ureq::AgentBuilder::new()
262 .timeout_connect(Duration::from_secs(10))
263 .timeout_read(Duration::from_secs(30))
264 .timeout_write(Duration::from_secs(30))
265 .user_agent("pastebin-ai-cli/0.1.0")
266 .build();
267 Ok(Self {
268 agent,
269 auth_token: None,
270 delete_token: None,
271 delete_paste_id: None,
272 })
273 }
274
275 pub fn with_token(token: &str) -> Result<Self> {
279 let mut client = Self::new()?;
280 client.auth_token = Some(token.to_string());
281 Ok(client)
282 }
283
284 pub fn with_delete_token(paste_id: &str, token: &str) -> Result<Self> {
288 let mut client = Self::new()?;
289 client.delete_token = Some(token.to_string());
290 client.delete_paste_id = Some(paste_id.to_string());
291 Ok(client)
292 }
293
294 pub fn create_paste(&self, request: CreatePasteRequest) -> Result<PasteCreated> {
298 let req = self.agent.post(&format!("{}/pastes", BASE_URL)).send_json(&request)?;
299
300 let status = req.status();
301 let body = req.into_string()?;
302
303 if (200..300).contains(&status) {
304 Ok(serde_json::from_str(&body)?)
305 } else {
306 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
307 error: format!("HTTP {}", status),
308 _retry_after: None,
309 });
310 anyhow::bail!("API error: {}", err.error);
311 }
312 }
313
314 pub fn get_paste(&self, id: &str) -> Result<Paste> {
319 let url = format!("{}/pastes/{}", BASE_URL, id);
320
321 let resp = self.agent.get(&url).call()?;
322
323 let status = resp.status();
324 let body = resp.into_string()?;
325
326 if (200..300).contains(&status) {
327 Ok(serde_json::from_str(&body)?)
328 } else if status == 404 {
329 anyhow::bail!("Paste not found: {}", id);
330 } else {
331 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
332 error: format!("HTTP {}", status),
333 _retry_after: None,
334 });
335 anyhow::bail!("API error: {}", err.error);
336 }
337 }
338
339 pub fn list_pastes(&self, limit: usize) -> Result<Vec<Paste>> {
343 let token = self.auth_token.as_ref()
344 .ok_or_else(|| anyhow::anyhow!("Authentication required"))?;
345
346 let resp = self.agent
347 .get(&format!("{}/account/pastes", BASE_URL))
348 .set("Authorization", &format!("Bearer {}", token))
349 .call()?;
350
351 let status = resp.status();
352 let body = resp.into_string()?;
353
354 if (200..300).contains(&status) {
355 let pastes: Vec<Paste> = serde_json::from_str(&body)?;
356 Ok(pastes.into_iter().take(limit).collect())
357 } else {
358 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
359 error: format!("HTTP {}", status),
360 _retry_after: None,
361 });
362 anyhow::bail!("API error: {}", err.error);
363 }
364 }
365
366 pub fn delete_paste(&self, id: &str) -> Result<()> {
372 let (_url, req) = if let (Some(token), Some(paste_id)) = (&self.delete_token, &self.delete_paste_id) {
374 if paste_id == id {
375 let url = format!("{}/pastes/{}", BASE_URL, id);
377 let req = self.agent.delete(&url).set("X-Delete-Token", token);
378 (url, req)
379 } else if let Some(auth) = &self.auth_token {
380 let url = format!("{}/account/pastes/{}", BASE_URL, id);
382 let req = self.agent.delete(&url).set("Authorization", &format!("Bearer {}", auth));
383 (url, req)
384 } else {
385 anyhow::bail!("No valid authentication for delete");
386 }
387 } else if let Some(auth) = &self.auth_token {
388 let url = format!("{}/account/pastes/{}", BASE_URL, id);
390 let req = self.agent.delete(&url).set("Authorization", &format!("Bearer {}", auth));
391 (url, req)
392 } else {
393 anyhow::bail!("No valid authentication for delete");
394 };
395
396 let resp = req.call()?;
397 let status = resp.status();
398
399 if (200..300).contains(&status) || status == 204 {
400 Ok(())
401 } else if status == 404 {
402 anyhow::bail!("Paste not found: {}", id);
403 } else {
404 let body = resp.into_string()?;
405 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
406 error: format!("HTTP {}", status),
407 _retry_after: None,
408 });
409 anyhow::bail!("API error: {}", err.error);
410 }
411 }
412}