nttb-api 0.1.0

A interface for interacting with the NTTB Api
Documentation


use std::collections::BTreeMap;
use std::result;


use serde::{Serialize, Deserialize, de::DeserializeOwned};
use serde_with::serde_as;
use serde_with::json::JsonString;
use type_get_info::EloPerSeason;
mod errors;
mod type_get_info;
mod type_search;
mod type_event_get_info;

const API_URL:&'static str = "https://www.nttb-ranglijsten.nl/dwf/v2/";
const REST_PWD:&'static str = "dwf";
const PASS_PHRASE:&'static str = "In dubbelspelen, met uitzondering van het bepaalde in 2.8.3, moet de serveerder de bal eerst in het spel brengen, waarna de ontvanger de bal moet retourneren. Vervolgens zal de partner van de serveerder moeten terugslaan, terwijl daarna de partner van de ontvanger aan de beurt is om de bal te retourner (Spelregels 2.8.2)";
const JWT_HEADER_STR: &'static str = r#"{"alg":"HS256","typ":"JWT"}"#;
const JWT_GUEST_DATA_STR: &'static str = r#"{"username":0}"#;


pub struct NTTBApi {
    cached_jwt:String,
    cached_username:String,
    request_client:reqwest::Client
}

pub enum ApiRequest {
    SearchPlayer(String),
    GetInfoPlayer(String),
    GetInfoCompetition(String)
}

impl ApiRequest {
    pub fn type_name(&self)->&'static str {
        match self {
            ApiRequest::SearchPlayer(_) => "search_player",
            ApiRequest::GetInfoPlayer(_) => "get_results",
            ApiRequest::GetInfoCompetition(_) => "get_wcomp",
        }
    }
    pub fn params_map(self)->BTreeMap<&'static str,String>{
        let mut map = BTreeMap::<&'static str,String>::new();
        match self {
            ApiRequest::SearchPlayer(search) => {
                map.insert("search", search);
            },
            ApiRequest::GetInfoPlayer(player_id) => {
                map.insert("user", player_id);
                map.insert("comp", "probably doesn't actually have to be a valid competition string".to_string());
            },
            ApiRequest::GetInfoCompetition(event_id) => {
                map.insert("pID", event_id);
            },
            
            
        }
        return map
    }
}


#[derive(Serialize)]
struct ApiRequestBody<'a>{
    jwt:&'a String,
    username:&'a String,
    #[serde(flatten)]
    data:BTreeMap<&'static str,String>
}



#[derive(Deserialize,Debug)]
pub struct RequestResults<T>{
    ///If any errors occurred whilst querying 
    error:String,
    #[serde(flatten)]
    result:Option<T>
}
#[derive(Deserialize,Debug)]
pub struct RequestResultsMustMatch<T>{
    ///If any errors occurred whilst querying 
    error:String,
    #[serde(flatten)]
    result:T
}


pub type APIResult<T> = Result<T,errors::APIRequestError>;

impl NTTBApi {


    pub async fn event_get_info(&self,event_id:u64)->APIResult<type_event_get_info::EventGetInfoResult>{
        // println!("{}",self.api_request_text(ApiRequest::GetInfoCompetition(event_id.to_string())).await.unwrap());
        self.api_request_must_match(ApiRequest::GetInfoCompetition(event_id.to_string())).await
    }

    pub async fn search_player<T:Into<String>>(&self,name:T)->APIResult<type_search::SearchResult>{
        self.api_request(ApiRequest::SearchPlayer(name.into())).await
    }

    pub async fn get_info(&self,bondsnumber:u64)->APIResult<type_get_info::Info>{
        // println!("{}",self.api_request_text(ApiRequest::GetInfoPlayer(bondsnumber.to_string())).await.unwrap());
        let res = self.api_request::<type_get_info::GetInfoResult>(ApiRequest::GetInfoPlayer(bondsnumber.to_string())).await?;
        let mut elo:Option<type_get_info::EloPerSeason> = None;
        let mut results:Vec<type_get_info::Event> = vec![];

        for e in res.results {
            match e {
                type_get_info::GetInfoContent::Event(event) => results.push(event),
                type_get_info::GetInfoContent::EloPerSeason(eps) => elo = Some(EloPerSeason(eps)),
            }
        }

        return Ok(type_get_info::Info {
            elo: elo.unwrap_or(EloPerSeason(BTreeMap::new())),
            results: results,
            player_info: res.player_info,
        })
    }

    pub async fn api_request<OutType:DeserializeOwned>(&self,request:ApiRequest)->Result<OutType,errors::APIRequestError>{
        let url = format!("{}?{}",API_URL,request.type_name());
        let body = ApiRequestBody{
            jwt: &self.cached_jwt,
            username: &self.cached_username,
            data: request.params_map(),
        };
        let response = self.request_client.post(url)
            .form(&body)
            .send().await?;
        let res = response.json::<RequestResults<OutType>>().await?;
        return  Ok(res.result.ok_or(errors::APIError{msg:res.error})?);
        // return Ok(res.result)
    }
    pub async fn api_request_must_match<OutType:DeserializeOwned>(&self,request:ApiRequest)->Result<OutType,errors::APIRequestError>{
        let url = format!("{}?{}",API_URL,request.type_name());
        let body = ApiRequestBody{
            jwt: &self.cached_jwt,
            username: &self.cached_username,
            data: request.params_map(),
        };
        let response = self.request_client.post(url)
            .form(&body)
            .send().await?;
        let res = response.json::<RequestResultsMustMatch<OutType>>().await?;
        return  Ok(res.result);
        // return Ok(res.result)
    }
    pub async fn api_request_text(&self,request:ApiRequest)->Result<String,errors::APIRequestError>{
        let url = format!("{}?{}",API_URL,request.type_name());
        let body = ApiRequestBody{
            jwt: &self.cached_jwt,
            username: &self.cached_username,
            data: request.params_map(),
        };
        let response = self.request_client.post(url)
            .form(&body)
            .send().await?;
        let res = response.text().await?;
        return  Ok(res);
    }

    pub fn guest()->Result<Self,errors::ApiCreationError>{
        let client = reqwest::Client::builder().build()?;
        Ok(Self {
            cached_jwt: Self::guest_jwt()?,
            cached_username: "0".to_string(),
            request_client: client,
        })
    }

    ///
    ///  Create The Guest JWT which will always be the same
    /// 
    pub fn guest_jwt()->Result<String,errors::JWTError>{
        use sha2::{Sha256,Digest};
        
        use hmac::{Hmac,Mac};
        use base64ct::{Base64,Base64UrlUnpadded,Encoding};
        let hash = Sha256::new()
            .chain_update(PASS_PHRASE.as_bytes())
            .finalize();
        // let mut base_64_enc_dest = [];
        let signature_password = &Base64::encode_string(&hash[..])[8..28];
        let header = Base64UrlUnpadded::encode_string(JWT_HEADER_STR.as_bytes());
        let body = Base64UrlUnpadded::encode_string(JWT_GUEST_DATA_STR.as_bytes());
        // let body = Base64Url::encode_string(JWT_GUEST_DATA_STR.as_bytes());

        let mut signature_mac = Hmac::<Sha256>::new_from_slice(signature_password.as_bytes())?;
        signature_mac.update(format!("{}.{}",header,body).as_bytes());
        let signature_bytes = signature_mac.finalize().into_bytes();
        let signature = Base64UrlUnpadded::encode_string(&signature_bytes[..]);

        return Ok(format!("{}.{}.{}",header,body,signature));
    }
}