musket 0.23.0

Musket is a command line interface to send a URL to several destinations.
Documentation
use super::{Bookmark, SourceError};
use oauth1::Token;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use tracing::debug;

#[derive(Default, Serialize, Deserialize)]
pub struct InstapaperConfiguration {
    pub username: String,
    pub password: String,
    pub consumer_key: String,
    pub consumer_secret: String,
}
pub struct Instapaper {
    pub configuration: InstapaperConfiguration,
}

impl From<reqwest::Error> for SourceError {
    fn from(e: reqwest::Error) -> Self {
        SourceError::Instapaper {
            message: format!("Something went wrong when connecting to Instapaper {e}."),
        }
    }
}

impl From<serde_json::Error> for SourceError {
    fn from(e: serde_json::Error) -> Self {
        SourceError::Instapaper {
            message: format!("Something went wrong when parsing the JSON response {e}."),
        }
    }
}

impl Instapaper {
    pub fn new(username: &str, password: &str, consumer_key: &str, consumer_secret: &str) -> Self {
        Instapaper {
            configuration: InstapaperConfiguration {
                username: username.to_string(),
                password: password.to_string(),
                consumer_key: consumer_key.to_string(),
                consumer_secret: consumer_secret.to_string(),
            },
        }
    }

    pub async fn get_bookmark(&self) -> Result<Bookmark, SourceError> {
        debug!("Inside get_bookmark function");

        let tokens = self.get_oauth_tokens().await?;

        let mut params: HashMap<&str, Cow<str>> = HashMap::new();
        params.insert("limit", Cow::Borrowed("1"));
        params.insert("tag", Cow::Borrowed("musket"));

        let response = Self::do_request(
            "bookmarks/list",
            &self.configuration.consumer_key,
            &self.configuration.consumer_secret,
            &tokens[0],
            &tokens[1],
            Some(params),
        )
        .await?;

        if !response.status().is_success() {
            return Err(SourceError::Instapaper {
                message: "Something went wrong when getting bookmarks from Instapaper.".to_string(),
            });
        }

        let body = response.text().await?;
        let json: Value = serde_json::from_str(&body)?;
        let bookmark = Self::parse_bookmark_response(&json);

        Ok(bookmark)
    }

    pub async fn delete_bookmark(&self, bookmark_id: i64) -> Result<(), SourceError> {
        debug!("Inside delete_bookmark function");

        let tokens = self.get_oauth_tokens().await?;

        let mut params: HashMap<&str, Cow<str>> = HashMap::new();
        params.insert("bookmark_id", Cow::Owned(bookmark_id.to_string()));

        let response = Self::do_request(
            "bookmarks/delete",
            &self.configuration.consumer_key,
            &self.configuration.consumer_secret,
            &tokens[0],
            &tokens[1],
            Some(params),
        )
        .await?;

        if !response.status().is_success() {
            return Err(SourceError::Instapaper {
                message: "Something went wrong when deleting bookmarks from Instapaper."
                    .to_string(),
            });
        }

        Ok(())
    }

    async fn get_oauth_tokens(&self) -> Result<Vec<String>, SourceError> {
        debug!("Inside get_oauth_tokens function");

        let mut params: HashMap<&str, Cow<str>> = HashMap::new();
        params.insert(
            "x_auth_username",
            Cow::Borrowed(&self.configuration.username),
        );
        params.insert(
            "x_auth_password",
            Cow::Borrowed(&self.configuration.password),
        );
        params.insert("x_auth_mode", Cow::Borrowed("client_auth"));

        let response = Self::do_request(
            "oauth/access_token",
            &self.configuration.consumer_key,
            &self.configuration.consumer_secret,
            "",
            "",
            Some(params),
        )
        .await?;

        if !response.status().is_success() {
            return Err(SourceError::Instapaper {
                message: "Something went wrong when getting oauth tokens from Instapaper."
                    .to_string(),
            });
        }

        let mut tokens: Vec<String> = Vec::new();
        let tokens_string = response.text().await?;
        let re = regex::Regex::new(r"oauth_token_secret=([^&]*)&oauth_token=([^&]*)").unwrap();
        if let Some(captures) = re.captures(&tokens_string) {
            let oauth_token_secret = captures.get(1).map_or("", |m| m.as_str());
            let oauth_token = captures.get(2).map_or("", |m| m.as_str());
            tokens.push(oauth_token.to_string());
            tokens.push(oauth_token_secret.to_string());
        }

        Ok(tokens)
    }

    async fn do_request<'a>(
        url: &str,
        consumer_key: &str,
        consumer_secret: &str,
        access_token: &str,
        access_token_secret: &str,
        params: Option<HashMap<&'a str, Cow<'a, str>>>,
    ) -> reqwest::Result<reqwest::Response> {
        debug!("Inside do_request function");

        let request_url = format!("https://www.instapaper.com/api/1/{url}");
        let client = Client::new();
        let response = client
            .post(&request_url)
            .form(&params)
            .header(
                reqwest::header::AUTHORIZATION,
                oauth1::authorize(
                    "POST",
                    &request_url,
                    &Token::new(consumer_key, consumer_secret),
                    Some(&Token::new(access_token, access_token_secret)),
                    params,
                ),
            )
            .send()
            .await?;

        Ok(response)
    }

    fn parse_bookmark_response(json: &Value) -> Bookmark {
        let tags_array = json[2]["tags"].as_array();
        let tags = match tags_array {
            Some(tags) => tags
                .iter()
                .filter(|tag| tag["name"] != "musket")
                .map(|tag| tag["name"].as_str().unwrap_or_default().to_string())
                .collect::<Vec<_>>(),
            None => vec![],
        };

        Bookmark {
            id: json[2]["bookmark_id"].as_i64().unwrap_or_default(),
            url: json[2]["url"].as_str().unwrap_or_default().to_string(),
            tags,
        }
    }
}