1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4use ureq;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum PasteVisibility {
9 Public,
10 Unlisted,
11 Private,
12}
13
14impl Default for PasteVisibility {
15 fn default() -> Self {
16 PasteVisibility::Unlisted
17 }
18}
19
20impl std::str::FromStr for PasteVisibility {
21 type Err = String;
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 match s.to_lowercase().as_str() {
24 "public" => Ok(PasteVisibility::Public),
25 "unlisted" => Ok(PasteVisibility::Unlisted),
26 "private" => Ok(PasteVisibility::Private),
27 _ => Err(format!("Invalid visibility: {}", s)),
28 }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum PasteExpiration {
35 #[serde(rename = "10m")]
36 Min10,
37 #[serde(rename = "1h")]
38 Hour1,
39 #[serde(rename = "1d")]
40 Day1,
41 #[serde(rename = "1w")]
42 Week1,
43 #[serde(rename = "2w")]
44 Week2,
45 #[serde(rename = "1m")]
46 Month1,
47 #[serde(rename = "6m")]
48 Month6,
49 #[serde(rename = "1y")]
50 Year1,
51 Never,
52}
53
54impl Default for PasteExpiration {
55 fn default() -> Self {
56 PasteExpiration::Week1
57 }
58}
59
60impl std::str::FromStr for PasteExpiration {
61 type Err = String;
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 match s.to_lowercase().as_str() {
64 "10m" => Ok(PasteExpiration::Min10),
65 "1h" => Ok(PasteExpiration::Hour1),
66 "1d" => Ok(PasteExpiration::Day1),
67 "1w" => Ok(PasteExpiration::Week1),
68 "2w" => Ok(PasteExpiration::Week2),
69 "1m" => Ok(PasteExpiration::Month1),
70 "6m" => Ok(PasteExpiration::Month6),
71 "1y" => Ok(PasteExpiration::Year1),
72 "never" => Ok(PasteExpiration::Never),
73 _ => Err(format!("Invalid expiration: {}", s)),
74 }
75 }
76}
77
78impl PasteExpiration {
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 PasteExpiration::Min10 => "10m",
82 PasteExpiration::Hour1 => "1h",
83 PasteExpiration::Day1 => "1d",
84 PasteExpiration::Week1 => "1w",
85 PasteExpiration::Week2 => "2w",
86 PasteExpiration::Month1 => "1m",
87 PasteExpiration::Month6 => "6m",
88 PasteExpiration::Year1 => "1y",
89 PasteExpiration::Never => "never",
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(rename_all = "lowercase")]
96pub enum PasteCategory {
97 Code,
98 Logs,
99 Configuration,
100 Data,
101 Documentation,
102 Other,
103}
104
105impl Default for PasteCategory {
106 fn default() -> Self {
107 PasteCategory::Code
108 }
109}
110
111impl std::str::FromStr for PasteCategory {
112 type Err = String;
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 match s.to_lowercase().as_str() {
115 "code" => Ok(PasteCategory::Code),
116 "logs" => Ok(PasteCategory::Logs),
117 "configuration" => Ok(PasteCategory::Configuration),
118 "data" => Ok(PasteCategory::Data),
119 "documentation" => Ok(PasteCategory::Documentation),
120 "other" => Ok(PasteCategory::Other),
121 _ => Err(format!("Invalid category: {}", s)),
122 }
123 }
124}
125
126#[derive(Debug, Serialize)]
127pub struct CreatePasteRequest {
128 pub content: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub title: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub language: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub visibility: Option<PasteVisibility>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub expiration: Option<PasteExpiration>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub category: Option<PasteCategory>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub tags: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub max_views: Option<u32>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub password: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub burn_after_read: Option<bool>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub folder_id: Option<u64>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub encrypted: Option<bool>,
151}
152
153#[derive(Debug, Deserialize)]
154pub struct PasteCreated {
155 pub id: String,
156 pub url: String,
157 pub raw_url: String,
158 pub delete_token: String,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub expires_at: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub max_views: Option<u32>,
163}
164
165#[derive(Debug, Deserialize)]
166pub struct Paste {
167 pub id: String,
168 pub title: Option<String>,
169 pub content: String,
170 pub language: String,
171 pub visibility: String,
172 pub views: u32,
173 pub created_at: String,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub expires_at: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub max_views: Option<u32>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub updated_at: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub url: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub category: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub tags: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub burn_after_read: Option<bool>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub encrypted: Option<bool>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub folder_id: Option<u64>,
192}
193
194#[derive(Debug, Deserialize)]
195struct ErrorResponse {
196 error: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 retry_after: Option<u64>,
199}
200
201const BASE_URL: &str = "https://pastebin.ai/api/v1";
202
203pub struct PastebinClient {
204 agent: ureq::Agent,
205 auth_token: Option<String>,
206 delete_token: Option<String>,
207 delete_paste_id: Option<String>,
208}
209
210impl PastebinClient {
211 pub fn new() -> Result<Self> {
212 let agent = ureq::AgentBuilder::new()
213 .timeout_connect(Duration::from_secs(10))
214 .timeout_read(Duration::from_secs(30))
215 .timeout_write(Duration::from_secs(30))
216 .user_agent("pastebin-ai-cli/0.1.0")
217 .build();
218 Ok(Self {
219 agent,
220 auth_token: None,
221 delete_token: None,
222 delete_paste_id: None,
223 })
224 }
225
226 pub fn with_token(token: &str) -> Result<Self> {
227 let mut client = Self::new()?;
228 client.auth_token = Some(token.to_string());
229 Ok(client)
230 }
231
232 pub fn with_delete_token(paste_id: &str, token: &str) -> Result<Self> {
233 let mut client = Self::new()?;
234 client.delete_token = Some(token.to_string());
235 client.delete_paste_id = Some(paste_id.to_string());
236 Ok(client)
237 }
238
239 fn request_with_auth(&self, req: ureq::Request) -> ureq::Request {
240 if let Some(token) = &self.auth_token {
241 req.set("Authorization", &format!("Bearer {}", token))
242 } else {
243 req
244 }
245 }
246
247 pub fn create_paste(&self, request: CreatePasteRequest) -> Result<PasteCreated> {
248 let mut req = self.agent.post(&format!("{}/pastes", BASE_URL)).send_json(&request)?;
249
250 let status = req.status();
251 let body = req.into_string()?;
252
253 if (200..300).contains(&status) {
254 Ok(serde_json::from_str(&body)?)
255 } else {
256 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
257 error: format!("HTTP {}", status),
258 retry_after: None,
259 });
260 anyhow::bail!("API error: {}", err.error);
261 }
262 }
263
264 pub fn get_paste(&self, id: &str) -> Result<Paste> {
265 let url = format!("{}/pastes/{}", BASE_URL, id);
266
267 let resp = self.agent.get(&url).call()?;
268
269 let status = resp.status();
270 let body = resp.into_string()?;
271
272 if (200..300).contains(&status) {
273 Ok(serde_json::from_str(&body)?)
274 } else if status == 404 {
275 anyhow::bail!("Paste not found: {}", id);
276 } else {
277 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
278 error: format!("HTTP {}", status),
279 retry_after: None,
280 });
281 anyhow::bail!("API error: {}", err.error);
282 }
283 }
284
285 pub fn list_pastes(&self, limit: usize) -> Result<Vec<Paste>> {
286 let token = self.auth_token.as_ref()
287 .ok_or_else(|| anyhow::anyhow!("Authentication required"))?;
288
289 let resp = self.agent
290 .get(&format!("{}/account/pastes", BASE_URL))
291 .set("Authorization", &format!("Bearer {}", token))
292 .call()?;
293
294 let status = resp.status();
295 let body = resp.into_string()?;
296
297 if (200..300).contains(&status) {
298 let pastes: Vec<Paste> = serde_json::from_str(&body)?;
299 Ok(pastes.into_iter().take(limit).collect())
300 } else {
301 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
302 error: format!("HTTP {}", status),
303 retry_after: None,
304 });
305 anyhow::bail!("API error: {}", err.error);
306 }
307 }
308
309 pub fn delete_paste(&self, id: &str) -> Result<()> {
310 let (_url, req) = if let (Some(token), Some(paste_id)) = (&self.delete_token, &self.delete_paste_id) {
312 if paste_id == id {
313 let url = format!("{}/pastes/{}", BASE_URL, id);
315 let req = self.agent.delete(&url).set("X-Delete-Token", token);
316 (url, req)
317 } else if let Some(auth) = &self.auth_token {
318 let url = format!("{}/account/pastes/{}", BASE_URL, id);
320 let req = self.agent.delete(&url).set("Authorization", &format!("Bearer {}", auth));
321 (url, req)
322 } else {
323 anyhow::bail!("No valid authentication for delete");
324 }
325 } else if let Some(auth) = &self.auth_token {
326 let url = format!("{}/account/pastes/{}", BASE_URL, id);
328 let req = self.agent.delete(&url).set("Authorization", &format!("Bearer {}", auth));
329 (url, req)
330 } else {
331 anyhow::bail!("No valid authentication for delete");
332 };
333
334
335 let resp = req.call()?;
336 let status = resp.status();
337
338 if (200..300).contains(&status) || status == 204 {
339 Ok(())
340 } else if status == 404 {
341 anyhow::bail!("Paste not found: {}", id);
342 } else {
343 let body = resp.into_string()?;
344 let err: ErrorResponse = serde_json::from_str(&body).unwrap_or(ErrorResponse {
345 error: format!("HTTP {}", status),
346 retry_after: None,
347 });
348 anyhow::bail!("API error: {}", err.error);
349 }
350 }
351}