nttb_api/
lib.rs

1
2
3use std::collections::BTreeMap;
4use std::result;
5
6
7use serde::{Serialize, Deserialize, de::DeserializeOwned};
8use serde_with::serde_as;
9use serde_with::json::JsonString;
10use type_get_info::EloPerSeason;
11mod errors;
12mod type_get_info;
13mod type_search;
14mod type_event_get_info;
15
16const API_URL:&'static str = "https://www.nttb-ranglijsten.nl/dwf/v2/";
17const REST_PWD:&'static str = "dwf";
18const 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)";
19const JWT_HEADER_STR: &'static str = r#"{"alg":"HS256","typ":"JWT"}"#;
20const JWT_GUEST_DATA_STR: &'static str = r#"{"username":0}"#;
21
22
23pub struct NTTBApi {
24    cached_jwt:String,
25    cached_username:String,
26    request_client:reqwest::Client
27}
28
29pub enum ApiRequest {
30    SearchPlayer(String),
31    GetInfoPlayer(String),
32    GetInfoCompetition(String)
33}
34
35impl ApiRequest {
36    pub fn type_name(&self)->&'static str {
37        match self {
38            ApiRequest::SearchPlayer(_) => "search_player",
39            ApiRequest::GetInfoPlayer(_) => "get_results",
40            ApiRequest::GetInfoCompetition(_) => "get_wcomp",
41        }
42    }
43    pub fn params_map(self)->BTreeMap<&'static str,String>{
44        let mut map = BTreeMap::<&'static str,String>::new();
45        match self {
46            ApiRequest::SearchPlayer(search) => {
47                map.insert("search", search);
48            },
49            ApiRequest::GetInfoPlayer(player_id) => {
50                map.insert("user", player_id);
51                map.insert("comp", "probably doesn't actually have to be a valid competition string".to_string());
52            },
53            ApiRequest::GetInfoCompetition(event_id) => {
54                map.insert("pID", event_id);
55            },
56            
57            
58        }
59        return map
60    }
61}
62
63
64#[derive(Serialize)]
65struct ApiRequestBody<'a>{
66    jwt:&'a String,
67    username:&'a String,
68    #[serde(flatten)]
69    data:BTreeMap<&'static str,String>
70}
71
72
73
74#[derive(Deserialize,Debug)]
75pub struct RequestResults<T>{
76    ///If any errors occurred whilst querying 
77    error:String,
78    #[serde(flatten)]
79    result:Option<T>
80}
81#[derive(Deserialize,Debug)]
82pub struct RequestResultsMustMatch<T>{
83    ///If any errors occurred whilst querying 
84    error:String,
85    #[serde(flatten)]
86    result:T
87}
88
89
90pub type APIResult<T> = Result<T,errors::APIRequestError>;
91
92impl NTTBApi {
93
94
95    pub async fn event_get_info(&self,event_id:u64)->APIResult<type_event_get_info::EventGetInfoResult>{
96        // println!("{}",self.api_request_text(ApiRequest::GetInfoCompetition(event_id.to_string())).await.unwrap());
97        self.api_request_must_match(ApiRequest::GetInfoCompetition(event_id.to_string())).await
98    }
99
100    pub async fn search_player<T:Into<String>>(&self,name:T)->APIResult<type_search::SearchResult>{
101        self.api_request(ApiRequest::SearchPlayer(name.into())).await
102    }
103
104    pub async fn get_info(&self,bondsnumber:u64)->APIResult<type_get_info::Info>{
105        // println!("{}",self.api_request_text(ApiRequest::GetInfoPlayer(bondsnumber.to_string())).await.unwrap());
106        let res = self.api_request::<type_get_info::GetInfoResult>(ApiRequest::GetInfoPlayer(bondsnumber.to_string())).await?;
107        let mut elo:Option<type_get_info::EloPerSeason> = None;
108        let mut results:Vec<type_get_info::Event> = vec![];
109
110        for e in res.results {
111            match e {
112                type_get_info::GetInfoContent::Event(event) => results.push(event),
113                type_get_info::GetInfoContent::EloPerSeason(eps) => elo = Some(EloPerSeason(eps)),
114            }
115        }
116
117        return Ok(type_get_info::Info {
118            elo: elo.unwrap_or(EloPerSeason(BTreeMap::new())),
119            results: results,
120            player_info: res.player_info,
121        })
122    }
123
124    pub async fn api_request<OutType:DeserializeOwned>(&self,request:ApiRequest)->Result<OutType,errors::APIRequestError>{
125        let url = format!("{}?{}",API_URL,request.type_name());
126        let body = ApiRequestBody{
127            jwt: &self.cached_jwt,
128            username: &self.cached_username,
129            data: request.params_map(),
130        };
131        let response = self.request_client.post(url)
132            .form(&body)
133            .send().await?;
134        let res = response.json::<RequestResults<OutType>>().await?;
135        return  Ok(res.result.ok_or(errors::APIError{msg:res.error})?);
136        // return Ok(res.result)
137    }
138    pub async fn api_request_must_match<OutType:DeserializeOwned>(&self,request:ApiRequest)->Result<OutType,errors::APIRequestError>{
139        let url = format!("{}?{}",API_URL,request.type_name());
140        let body = ApiRequestBody{
141            jwt: &self.cached_jwt,
142            username: &self.cached_username,
143            data: request.params_map(),
144        };
145        let response = self.request_client.post(url)
146            .form(&body)
147            .send().await?;
148        let res = response.json::<RequestResultsMustMatch<OutType>>().await?;
149        return  Ok(res.result);
150        // return Ok(res.result)
151    }
152    pub async fn api_request_text(&self,request:ApiRequest)->Result<String,errors::APIRequestError>{
153        let url = format!("{}?{}",API_URL,request.type_name());
154        let body = ApiRequestBody{
155            jwt: &self.cached_jwt,
156            username: &self.cached_username,
157            data: request.params_map(),
158        };
159        let response = self.request_client.post(url)
160            .form(&body)
161            .send().await?;
162        let res = response.text().await?;
163        return  Ok(res);
164    }
165
166    pub fn guest()->Result<Self,errors::ApiCreationError>{
167        let client = reqwest::Client::builder().build()?;
168        Ok(Self {
169            cached_jwt: Self::guest_jwt()?,
170            cached_username: "0".to_string(),
171            request_client: client,
172        })
173    }
174
175    ///
176    ///  Create The Guest JWT which will always be the same
177    /// 
178    pub fn guest_jwt()->Result<String,errors::JWTError>{
179        use sha2::{Sha256,Digest};
180        
181        use hmac::{Hmac,Mac};
182        use base64ct::{Base64,Base64UrlUnpadded,Encoding};
183        let hash = Sha256::new()
184            .chain_update(PASS_PHRASE.as_bytes())
185            .finalize();
186        // let mut base_64_enc_dest = [];
187        let signature_password = &Base64::encode_string(&hash[..])[8..28];
188        let header = Base64UrlUnpadded::encode_string(JWT_HEADER_STR.as_bytes());
189        let body = Base64UrlUnpadded::encode_string(JWT_GUEST_DATA_STR.as_bytes());
190        // let body = Base64Url::encode_string(JWT_GUEST_DATA_STR.as_bytes());
191
192        let mut signature_mac = Hmac::<Sha256>::new_from_slice(signature_password.as_bytes())?;
193        signature_mac.update(format!("{}.{}",header,body).as_bytes());
194        let signature_bytes = signature_mac.finalize().into_bytes();
195        let signature = Base64UrlUnpadded::encode_string(&signature_bytes[..]);
196
197        return Ok(format!("{}.{}.{}",header,body,signature));
198    }
199}