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>{
error:String,
#[serde(flatten)]
result:Option<T>
}
#[derive(Deserialize,Debug)]
pub struct RequestResultsMustMatch<T>{
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>{
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>{
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})?);
}
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);
}
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,
})
}
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 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 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));
}
}