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 error:String,
78 #[serde(flatten)]
79 result:Option<T>
80}
81#[derive(Deserialize,Debug)]
82pub struct RequestResultsMustMatch<T>{
83 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 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 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 }
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 }
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 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 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 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}