flickr-api 0.2.2

Easy interface to the Flickr API
Documentation
use crate::Resultable;
use data_encoding::BASE64;
use hmac::{Hmac, Mac};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use url::form_urlencoded;

type HmacSha1 = Hmac<sha1::Sha1>;

/// OAuth token
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
pub struct Token {
    pub token: String,
    pub secret: String,
}

/// flickr API key
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
pub struct ApiKey {
    pub key: String,
    pub secret: String,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OauthAccessAnswer {
    Ok(OauthAccessGranted),
    Err(OauthErrorDescription),
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OauthTokenAnswer {
    Ok(OauthTokenGranted),
    Err(OauthErrorDescription),
}

impl Resultable<Token, String> for OauthTokenAnswer {
    fn to_result(self) -> Result<Token, String> {
        match self {
            OauthTokenAnswer::Ok(OauthTokenGranted {
                oauth_callback_confirmed: _,
                oauth_token: token,
                oauth_token_secret: secret,
            }) => Ok(Token { token, secret }),
            OauthTokenAnswer::Err(e) => Err(e.oauth_problem),
        }
    }
}

#[derive(Debug, Default, Deserialize)]
pub struct OauthAccessGranted {
    pub fullname: String,
    pub username: String,
    pub user_nsid: String,
    pub oauth_token: String,
    pub oauth_token_secret: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct OauthTokenGranted {
    pub oauth_callback_confirmed: String,
    pub oauth_token: String,
    pub oauth_token_secret: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct OauthErrorDescription {
    pub oauth_problem: String,
    #[serde(default)]
    pub debug_sbs: String,
}

impl Resultable<OauthAccessGranted, String> for OauthAccessAnswer {
    fn to_result(self) -> Result<OauthAccessGranted, String> {
        match self {
            OauthAccessAnswer::Ok(k) => Ok(k),
            OauthAccessAnswer::Err(e) => Err(e.oauth_problem),
        }
    }
}

impl Resultable<Token, String> for OauthAccessAnswer {
    fn to_result(self) -> Result<Token, String> {
        match self {
            OauthAccessAnswer::Ok(OauthAccessGranted {
                fullname: _,
                username: _,
                user_nsid: _,
                oauth_token: token,
                oauth_token_secret: secret,
            }) => Ok(Token { token, secret }),
            OauthAccessAnswer::Err(e) => Err(e.oauth_problem),
        }
    }
}

/// The type of request that this URL expects
///
/// This is used by the signature algorithm and needs to match what is done later.
pub enum RequestTarget<'a> {
    Get(&'a str),
    Post(&'a str),
}

impl<'a> RequestTarget<'a> {
    fn uri(&'a self) -> &'a str {
        match self {
            RequestTarget::Get(val) => val,
            RequestTarget::Post(val) => val,
        }
    }
}

/// Prepares a request to be sent with authentication
///
/// This supplements the given parameters with a signature and necessary oauth fields. This methods
/// is dependent on the [RequestTarget] enum indicating which protocol to use as this influences
/// the signature.
pub fn build_request(
    target: RequestTarget,
    params: &mut Vec<(&'static str, String)>,
    api: &ApiKey,
    oauth: Option<&Token>,
) {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
        .to_string();

    let nonce = seconds[1..9].to_string();

    params.extend(vec![
        ("oauth_consumer_key", api.key.clone()),
        ("oauth_nonce", nonce),
        ("oauth_signature_method", "HMAC-SHA1".to_string()),
        ("oauth_timestamp", seconds),
        ("oauth_version", "1.0".to_string()),
    ]);

    let key: String;

    match &oauth {
        Some(value) => {
            params.extend(vec![("oauth_token", value.token.clone())]);
            key = format!("{}&{}", api.secret, value.secret)
        }
        None => key = format!("{}&", api.secret),
    };

    params.sort_by(|a, b| a.0.cmp(b.0));

    let to_sign = params
        .iter()
        .filter(|(k, _)| !vec!["photo"].contains(k))
        .map(|(a, b)| {
            format!(
                "{a}={}",
                form_urlencoded::byte_serialize(&b.as_bytes()).collect::<String>(),
            )
        })
        .join("&");

    let uri = target.uri();
    let method = match target {
        RequestTarget::Get(_) => "GET",
        RequestTarget::Post(_) => "POST",
    };

    let raw = format!(
        "{method}&{}&{}",
        form_urlencoded::byte_serialize(&uri.as_bytes()).collect::<String>(),
        form_urlencoded::byte_serialize(&to_sign.as_bytes()).collect::<String>()
    );

    let mut mac = HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
    mac.update(raw.as_bytes());
    let signature: String = BASE64.encode(&mac.finalize().into_bytes());

    params.push(("oauth_signature", signature));
}