pub use self::auth::auth;
pub use self::list::list;
pub use self::send::send;
mod auth {
use config::{Config, OutputFormat};
use net::http::tls_client;
use utils::console::*;
use hyper::Client;
use hyper::header::ContentType;
use mime::Mime;
use serde_json;
use std::io;
use std::io::Read;
use std::str;
use webbrowser;
static REDIRECT_URI: &'static str = "https://lukaspustina.github.io/rat/redirects/pocket.html";
error_chain! {
errors {
HttpAuthCallFailed {
description("failed to make HTTP auth call")
display("failed to make HTTP auth call")
}
}
}
header! { (XAccept, "X-Accept") => [Mime] }
#[derive(Serialize, Debug)]
struct CodeRequest<'a> {
consumer_key: &'a str,
redirect_uri: &'a str,
}
#[derive(Deserialize, Debug)]
struct Code {
code: String,
}
#[derive(Serialize, Debug)]
struct TokenRequest<'a> {
consumer_key: &'a str,
code: &'a str,
}
#[derive(Deserialize, Debug)]
struct Token {
access_token: String,
username: String,
}
pub fn auth(config: &Config, open_browser: bool) -> Result<()> {
do_auth(config, open_browser).chain_err(|| ErrorKind::HttpAuthCallFailed)
}
fn do_auth(config: &Config, open_browser: bool) -> Result<()> {
let client = tls_client().chain_err(|| "Failed to create TLS client")?;
let code = get_code(&client, config)?;
web_auth(&code, open_browser)?;
let token = exchange_token(&client, &code, config)?;
msgln(format!("Received access token for user '{}'. Please add the following line to your configuration, section '[pocket]'."
, token.username));
msgln(format!("\naccess_token = '{}'\n", token.access_token));
Ok(())
}
fn get_code(client: &Client, config: &Config) -> Result<Code> {
info("Requesting authentication code ...");
let code_request = CodeRequest { consumer_key: &config.pocket.consumer_key, redirect_uri: REDIRECT_URI };
let code_request_json = serde_json::to_string(&code_request).chain_err(|| "JSON serialization failed")?;
let url = "https://getpocket.com/v3/oauth/request";
let mut response = client
.post(url)
.header(XAccept(mime!(Application / Json)))
.header(ContentType(mime!(Application / Json)))
.body(&code_request_json)
.send()
.chain_err(|| "Failed to send HTTP request")?;
let mut buffer = Vec::new();
response.read_to_end(&mut buffer).chain_err(|| "Failed to read HTTP response")?;
if config.general.output_format == OutputFormat::JSON {
info("Received response:");
msgln(str::from_utf8(&buffer).chain_err(|| "Failed to print buffer")?);
}
let code: Code = serde_json::from_slice(&buffer).chain_err(|| "JSON parsing failed")?;
Ok(code)
}
fn web_auth(code: &Code, open_browser: bool) -> Result<()> {
info("Authorizing code via Pocket ...");
let auth_url = format!("https://getpocket.com/auth/authorize?request_token={}&redirect_uri={}",
code.code, REDIRECT_URI);
if open_browser {
msg("Please authenticate in the web browser window and then press return ...");
webbrowser::open(&auth_url).chain_err(|| "Failed to open web browser")?;
} else {
msgln("Please authenticate at the following URL and then press return ...");
msgln(format!("\n\t{}\n", auth_url));
}
let mut input = String::new();
let _ = io::stdin().read_line(&mut input);
Ok(())
}
fn exchange_token(client: &Client, code: &Code, config: &Config) -> Result<Token> {
info("Requesting access token ...");
let token_request = TokenRequest { consumer_key: &config.pocket.consumer_key, code: &code.code };
let token_request_token = serde_json::to_string(&token_request).chain_err(|| "JSON serialization failed")?;
let url = "https://getpocket.com/v3/oauth/authorize";
let mut response = client
.post(url)
.header(XAccept(mime!(Application / Json)))
.header(ContentType(mime!(Application / Json)))
.body(&token_request_token)
.send()
.chain_err(|| "Failed to send HTTP request")?;
let mut buffer = Vec::new();
response.read_to_end(&mut buffer).chain_err(|| "Failed to read HTTP response")?;
if config.general.output_format == OutputFormat::JSON {
info("Received response:");
msgln(str::from_utf8(&buffer).chain_err(|| "Failed to print buffer")?);
}
let token: Token = serde_json::from_slice(&buffer).chain_err(|| "JSON parsing failed")?;
Ok(token)
}
}
pub mod list {
use config::Config;
use net::http::tls_client;
use utils::console::*;
use hyper::header::ContentType;
use serde_json;
use std::collections::HashMap;
use std::io::Read;
use std::str;
use std::time::Duration;
error_chain! {
errors {
HttpListCallFailed {
description("HTTP call to list articles failed")
display("HTTP call to list articles failed")
}
}
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Debug)]
pub enum State {
unread,
archive,
all,
}
impl<'a> From<&'a str> for State {
fn from(s: &'a str) -> Self {
match s {
"archive" => State::archive,
"all" => State::all,
_ => State::unread,
}
}
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Debug)]
pub enum Sort {
newest,
oldest,
title,
site,
}
impl<'a> From<&'a str> for Sort {
fn from(s: &'a str) -> Self {
match s {
"oldest" => Sort::oldest,
"title" => Sort::title,
"site" => Sort::site,
_ => Sort::newest
}
}
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Debug)]
pub enum DetailType {
simple,
complete,
}
impl From<bool> for DetailType {
fn from(b: bool) -> Self {
if b {
DetailType::complete
} else {
DetailType::simple
}
}
}
#[allow(non_snake_case)]
#[derive(Serialize, Debug)]
pub struct Request<'a> {
pub consumer_key: &'a str,
pub access_token: &'a str,
#[serde(skip_serializing_if = "Option::is_none")] pub state: Option<State>,
#[serde(skip_serializing_if = "Option::is_none")] pub tag: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")] pub sort: Option<Sort>,
pub detailType: DetailType,
#[serde(skip_serializing_if = "Option::is_none")] pub search: Option<&'a str>,
}
pub fn list(config: &Config, request: &Request, since: Option<Duration>, until: Option<Duration>)
-> Result<String> {
info("Getting list of your articles ...");
let mut json = do_list(config, request).chain_err(|| ErrorKind::HttpListCallFailed)?;
if since.is_some() || until.is_some() {
let list: ListResult = serde_json::from_str(&json).chain_err(|| "JSON parsing failed")?;
info(format!("Filtering list of your {} article(s) ...", list.list.len()));
let list = list.filter(&since, &until);
json = serde_json::to_string(&list).chain_err(|| "JSON serialization failed")?;
}
Ok(json)
}
#[allow(unused_variables)] fn do_list(config: &Config, request: &Request) -> Result<String> {
let request_json = serde_json::to_string(&request).chain_err(|| "JSON serialization failed")?;
verboseln(format!("request = {}", request_json));
let client = tls_client().chain_err(|| "Failed to create TLS client")?;
let url = "https://getpocket.com/v3/get";
let mut response = client
.post(url)
.header(ContentType(mime!(Application / Json)))
.body(&request_json)
.send()
.chain_err(|| "Failed to finish HTTP request")?;
let mut buffer = Vec::new();
response.read_to_end(&mut buffer).chain_err(|| "Failed to read HTTP response")?;
let json = str::from_utf8(&buffer).chain_err(|| "Data copying failed.")?;
Ok(json.to_string())
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListResult {
pub status: i32,
pub complete: i32,
pub list: HashMap<String, Article>,
}
impl ListResult {
fn filter(self, since: &Option<Duration>, until: &Option<Duration>) -> Self {
let mut new_list: HashMap<String, Article> = HashMap::new();
for (k, v) in self.list {
if let Some(since) = *since {
if v.time_added().unwrap() < since { continue };
}
if let Some(until) = *until {
if v.time_added().unwrap() > until { continue };
}
new_list.insert(k, v);
}
ListResult { status: self.status, complete: self.complete, list: new_list }
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Article {
pub item_id: String,
pub resolved_title: String,
pub resolved_url: String,
pub time_added: String,
pub time_updated: String,
}
impl Article {
pub fn time_added(&self) -> Result<Duration> {
let secs: u64 = self.time_added.parse().chain_err(|| "Failed to parse time")?;
Ok(Duration::from_secs(secs))
}
}
}
pub mod send {
use net::http::tls_client;
use config::Config;
use serde_json;
use serde_urlencoded;
use std::io::Read;
use std::str;
error_chain! {
errors {
HttpActionCallFailed {
description("HTTP call for action failed")
display("HTTP call for action failed")
}
}
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Debug)]
#[allow(dead_code)]
enum Action {
archive,
read,
favorite,
unfavorite,
delete,
}
#[allow(non_snake_case)]
#[derive(Serialize, Debug)]
pub struct ActionRequest<'a> {
pub action: &'a str,
pub item_id: &'a str,
}
impl<'a> ActionRequest<'a> {
pub fn new(action: &'a str, item_id: &'a str) -> Self {
ActionRequest { action: action, item_id: item_id }
}
}
#[allow(unused_variables)] pub fn send(config: &Config, actions: &[ActionRequest]) -> Result<String> {
let json = do_send(config, actions).chain_err(|| ErrorKind::HttpActionCallFailed)?;
Ok(json)
}
fn do_send(config: &Config, actions: &[ActionRequest]) -> Result<String> {
let actions_json = serde_json::to_string(&actions).chain_err(|| "JSON serialization failed")?;
let parameters = &[
("actions", actions_json),
("access_token", config.pocket.access_token.as_ref().unwrap().to_string()),
("consumer_key", config.pocket.consumer_key.to_string())
];
let parameters_enc = serde_urlencoded::to_string(¶meters).chain_err(|| "URL serialization failed")?;
let url = format!("https://getpocket.com/v3/send?{}", parameters_enc);
let client = tls_client().chain_err(|| "Failed to create TLS client")?;
let mut response = client
.get(&url)
.send()
.chain_err(|| "Failed to finish HTTP request")?;
let mut buffer = Vec::new();
response.read_to_end(&mut buffer).chain_err(|| "Failed to read HTTP response")?;
let json = str::from_utf8(&buffer).chain_err(|| "Data copying failed.")?;
Ok(json.to_string())
}
}