1extern crate serde;
60#[macro_use]
61extern crate serde_derive;
62extern crate serde_json;
63#[macro_use]
64extern crate failure;
65extern crate oauth1;
66extern crate reqwest;
67extern crate url;
68
69#[cfg(test)]
70extern crate mockito;
71
72use std::borrow::Cow;
73use std::collections::HashMap;
74use std::iter::FromIterator;
75
76use oauth1::Token;
77use url::Url;
78
79use failure::Error;
80
81#[cfg(not(test))]
82const URL: &str = "https://www.instapaper.com";
83#[cfg(test)]
84const URL: &str = mockito::SERVER_URL;
85
86pub type Result<T> = std::result::Result<T, Error>;
87
88#[derive(Debug, Clone, Default)]
93pub struct Client {
94 pub consumer_key: String,
95 pub consumer_secret: String,
96 pub oauth_key: Option<String>,
97 pub oauth_secret: Option<String>,
98}
99
100#[derive(Deserialize, Debug, Clone, Default)]
103#[cfg_attr(test, derive(Serialize))]
104pub struct Bookmark {
105 pub title: String,
106 pub hash: String,
107 pub bookmark_id: i64,
108 pub progress_timestamp: f64,
109 pub description: String,
110 pub url: String,
111 pub time: f64,
112 pub starred: String,
113 #[serde(rename = "type")]
114 pub kind: String,
115 pub private_source: String,
116}
117
118#[derive(Deserialize, Debug, Clone, Default)]
120#[cfg_attr(test, derive(Serialize))]
121pub struct User {
122 pub username: String,
123 pub user_id: i64,
124 #[serde(rename = "type")]
125 pub kind: String,
126 #[serde(rename = "subscription_is_active")]
127 pub subscription: String,
128}
129
130#[derive(Deserialize, Debug, Clone, Default)]
132#[cfg_attr(test, derive(Serialize))]
133pub struct Highlight {
134 pub highlight_id: i64,
135 pub bookmark_id: i64,
136 pub text: String,
137 pub note: Option<String>,
138 pub time: i64,
139 pub position: i64,
140 #[serde(rename = "type")]
141 pub kind: String,
142}
143
144#[derive(Deserialize, Debug, Clone, Default)]
146#[cfg_attr(test, derive(Serialize))]
147pub struct List {
148 pub bookmarks: Vec<Bookmark>,
149 pub user: User,
150 pub highlights: Vec<Highlight>,
151 #[serde(default)]
152 pub delete_ids: Vec<i64>,
153}
154
155pub fn authenticate(username: &str, password: &str, consumer_key: &str, consumer_secret: &str) -> Result<Client> {
160 let mut params: HashMap<&str, Cow<str>> = HashMap::new();
161 params.insert("x_auth_username", Cow::Borrowed(username));
162 params.insert("x_auth_password", Cow::Borrowed(password));
163 params.insert("x_auth_mode", Cow::Borrowed("client_auth"));
164
165 let mut client = Client {
166 consumer_key: consumer_key.to_owned(),
167 consumer_secret: consumer_secret.to_owned(),
168 oauth_key: None,
169 oauth_secret: None,
170 };
171
172 let mut response = signed_request("oauth/access_token", params, &client)?;
173 let qline = response.text()?;
174
175 let qline = format!("https://junk.com/?{}", qline);
178 let url = Url::parse(&qline)?;
179 let query_params: HashMap<String, String> = HashMap::from_iter(url.query_pairs().into_owned());
180
181 let oauth_token = query_params.get("oauth_token");
182 let oauth_secret_token = query_params.get("oauth_token_secret");
183
184 if oauth_token.is_none() || oauth_secret_token.is_none() {
185 Err(format_err!("oauth_tokens not both in response: {}", qline))
186 } else {
187 client.oauth_key = Some(oauth_token.unwrap().to_owned());
188 client.oauth_secret = Some(oauth_secret_token.unwrap().to_owned());
189 Ok(client)
190 }
191}
192
193impl Client {
194 pub fn verify(&self) -> Result<User> {
196 let params = HashMap::new();
197 let mut response = signed_request("account/verify_credentials", params, self)?;
198 let mut users: Vec<User> = response.json()?;
199 Ok(users.remove(0))
200 }
201
202 pub fn archive(&self, bookmark_id: i64) -> Result<Bookmark> {
204 let bookmark_id_string = bookmark_id.to_string();
205 let mut params: HashMap<&str, Cow<str>> = HashMap::new();
206 params.insert("bookmark_id", Cow::Borrowed(&bookmark_id_string));
207 let mut response = signed_request("bookmarks/archive", params, self)?;
208 let mut bookmarks: Vec<Bookmark> = response.json()?;
209 Ok(bookmarks.remove(0))
210 }
211
212 pub fn bookmarks_in(&self, folder: &str) -> Result<List> {
215 let mut params: HashMap<&str, Cow<str>> = HashMap::new();
216 params.insert("limit", Cow::Borrowed("500"));
217 params.insert("folder_id", Cow::Borrowed(folder));
218 let mut response = signed_request("bookmarks/list", params, self)?;
219 response.json().map_err(|x| x.into())
220 }
221
222
223 pub fn bookmarks(&self) -> Result<List> {
225 self.bookmarks_in("unread")
226 }
227
228 pub fn add(&self, url: &str, title: &str, description: &str) -> Result<Bookmark> {
230 let mut params: HashMap<&str, Cow<str>> = HashMap::new();
231 params.insert("url", Cow::Borrowed(&url));
232 if !title.is_empty() {
233 params.insert("title", Cow::Borrowed(&title));
234 }
235 if !description.is_empty() {
236 params.insert("description", Cow::Borrowed(&description));
237 }
238
239 let mut response = signed_request("bookmarks/add", params, self)?;
240 let mut bookmarks: Vec<Bookmark> = response.json()?;
241 Ok(bookmarks.remove(0))
242 }
243}
244
245fn signed_request(
246 action: &str,
247 params: HashMap<&'static str, Cow<str>>,
248 client: &Client,
249) -> reqwest::Result<reqwest::Response> {
250 let http_client = reqwest::Client::new();
251 let url = format!("{}/api/1.1/{}", URL, action);
252 let empty = String::new();
253 let token = Token::new(
254 client.oauth_key.as_ref().unwrap_or(&empty),
255 client.oauth_secret.as_ref().unwrap_or(&empty),
256 );
257 let oauth: Option<&Token> = if client.oauth_key.as_ref().is_some() {
258 Some(&token)
259 } else {
260 None
261 };
262
263 let request = http_client
264 .post(&url)
265 .form(¶ms)
266 .header(
267 reqwest::header::AUTHORIZATION,
268 oauth1::authorize(
269 "POST",
270 &url,
271 &Token::new(
272 &client.consumer_key,
273 &client.consumer_secret,
274 ),
275 oauth,
276 Some(params),
277 ),
278 ).build()?;
279 http_client.execute(request)?.error_for_status()
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use mockito::mock;
286
287 fn client() -> Client {
288 Client {
289 consumer_key: String::new(),
290 consumer_secret: String::new(),
291 oauth_key: Some(String::new()),
292 oauth_secret: Some(String::new()),
293 }
294 }
295
296 #[test]
297 fn test_add_bookmark() {
298 let bookmark = vec![Bookmark {
299 title: "How I Read".to_string(),
300 ..Bookmark::default()
301 }];
302 let json = serde_json::to_string(&bookmark).unwrap();
303
304 let _m = mock("POST", "/api/1.1/bookmarks/add")
305 .with_status(201)
306 .with_header("content-type", "application/json")
307 .with_body(&json)
308 .create();
309
310 let result = client().add("https://sirupsen.com/read", "How I Read", "");
311 assert!(result.is_ok(), result.err().unwrap().to_string())
312 }
313
314 #[test]
315 fn test_add_bookmark_garbage_json() {
316 let _m = mock("POST", "/api/1.1/bookmarks/add")
317 .with_status(201)
318 .with_header("content-type", "application/json")
319 .with_body(r#"[garbageeee]"#)
320 .create();
321
322 let result = client().add("https://sirupsen.com/read", "How I Read", "");
323 assert!(result.is_err(), "Expected an error on garbage");
324 let err = result.err().unwrap();
325 assert_eq!("expected value at line 1 column 2", err.to_string());
326 }
327
328 #[test]
329 fn test_add_bookmark_error_code() {
330 let _m = mock("POST", "/api/1.1/bookmarks/add")
331 .with_status(500)
332 .with_header("content-type", "application/json")
333 .with_body(r#""#)
334 .create();
335
336 let result = client().add("https://sirupsen.com/read", "How I Read", "");
337 assert!(result.is_err(), "Expected an error on 500");
338 }
339
340 #[test]
341 fn test_authenticate() {
342 let _m = mock("POST", "/api/1.1/oauth/access_token")
343 .with_status(200)
344 .with_header("content-type", "application/text")
345 .with_body(r#"oauth_token=token&oauth_token_secret=secret"#)
346 .create();
347
348 let result = authenticate("username", "password", "key", "secret");
349 assert!(result.is_ok(), result.err().unwrap().to_string());
350 let client = result.unwrap();
351 assert_eq!("token", client.oauth_key.unwrap());
352 assert_eq!("secret", client.oauth_secret.unwrap());
353 }
354
355 #[test]
356 fn test_authenticate_reversed() {
357 let _m = mock("POST", "/api/1.1/oauth/access_token")
358 .with_status(200)
359 .with_header("content-type", "application/text")
360 .with_body(r#"oauth_token_secret=secret&oauth_token=token"#)
361 .create();
362
363 let result = authenticate("username", "password", "key", "secret");
364 assert!(result.is_ok(), result.err().unwrap().to_string());
365 let client = result.unwrap();
366 assert_eq!("token", client.oauth_key.unwrap());
367 assert_eq!("secret", client.oauth_secret.unwrap());
368 }
369
370 #[test]
371 fn test_authenticate_corrupted_qline() {
372 let _m = mock("POST", "/api/1.1/oauth/access_token")
373 .with_status(200)
374 .with_header("content-type", "application/text")
375 .with_body(r#"badqline"#)
376 .create();
377
378 let result = authenticate("username", "password", "key", "secret");
379 assert!(result.is_err(), "Expected an error");
380 let err = result.err().unwrap();
381 assert_eq!(
382 "oauth_tokens not both in response: https://junk.com/?badqline",
383 err.to_string()
384 )
385 }
386
387 #[test]
388 fn test_authenticate_qline_one_good_result() {
389 let _m = mock("POST", "/api/1.1/oauth/access_token")
390 .with_status(200)
391 .with_header("content-type", "application/text")
392 .with_body(r#"oauth_token=1&oauth_noep=walrus"#)
393 .create();
394
395 let result = authenticate("username", "password", "key", "secret");
396 assert!(result.is_err(), "Expected an error");
397 let err = result.err().unwrap();
398 assert_eq!(
399 "oauth_tokens not both in response: https://junk.com/?oauth_token=1&oauth_noep=walrus",
400 err.to_string()
401 )
402 }
403
404 #[test]
405 fn test_bookmarks() {
406 let list = List::default();
407 let json = serde_json::to_string(&list).unwrap();
408
409 let _m = mock("POST", "/api/1.1/bookmarks/list")
410 .with_status(201)
411 .with_header("content-type", "application/json")
412 .with_body(&json)
413 .create();
414
415 let result = client().bookmarks();
416 assert!(result.is_ok(), result.err().unwrap().to_string())
417 }
418
419 #[test]
420 fn test_bookmarks_error_status() {
421 let _m = mock("POST", "/api/1.1/bookmarks/list")
422 .with_status(500)
423 .with_header("content-type", "application/json")
424 .with_body("argh error!")
425 .create();
426
427 let result = client().bookmarks();
428 assert!(result.is_err(), "Expected an error on 500");
429 }
430
431 #[test]
432 fn test_verify() {
433 let user = vec![User::default()];
434 let json = serde_json::to_string(&user).unwrap();
435
436 let _m = mock("POST", "/api/1.1/account/verify_credentials")
437 .with_status(201)
438 .with_header("content-type", "application/json")
439 .with_body(&json)
440 .create();
441
442 let result = client().verify();
443 assert!(result.is_ok(), result.err().unwrap().to_string())
444 }
445
446 #[test]
447 fn test_verify_server_error() {
448 let _m = mock("POST", "/api/1.1/account/verify_credentials")
449 .with_status(500)
450 .with_header("content-type", "application/json")
451 .with_body("omgggg")
452 .create();
453
454 let result = client().verify();
455 assert!(result.is_err(), "Expected an error on 500");
456 }
457}