1use async_mutex::Mutex;
2use serde::de::DeserializeOwned;
3use url::Url;
4use crate::data::*;
5
6pub static BASE_URL: &str = "https://eapi.stalcraft.net/";
7pub static DEMO_URL: &str = "https://dapi.stalcraft.net";
8pub static DEMO_APP_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwibmJmIjoxNjczNzk3ODM4LCJleHAiOjQ4MjczOTc4MzgsImlhdCI6MTY3Mzc5NzgzOCwianRpIjoiYXhwbzAzenJwZWxkMHY5dDgzdzc1N2x6ajl1MmdyeHVodXVlb2xsZ3M2dml1YjVva3NwZTJ3eGFrdjJ1eWZxaDU5ZDE2ZTNlN2FqdW16Z3gifQ.ZNSsvwAX72xT5BzLqqYABuH2FGbOlfiXMK5aYO1H5llG51ZjcPvOYBDRR4HUoPZVLFY8jyFUsEXNM7SYz8qL9ePmLjJl6pib8FEtqVPmf9ldXvKkbaaaSp4KkJzsIEMY_Z5PejB2Vr-q-cL13KPgnLGUaSW-2X_sHPN7VZJNMjRgjw4mPiRZTe4CEpQq0BEcPrG6OLtU5qlZ6mLDJBjN2xtK0DI6xgmYriw_5qW1mj1nqF_ewtUiQ1KTVhDgXnaNUdkGsggAGqyicTei0td6DTKtnl3noD5VkipWn_CwSqb2Mhm16I9BPfX_d5ARzWrnrwPRUf6PA_7LipNU6KkkW0mhZfmwEPTm_sXPus0mHPENoVZArdFT3L5sOYBcpqwvVIEtxRUTdcsKp-y-gSzao5muoyPVoCc2LEeHEWx0cIi9spsZ46SPRQpN4baVFp7y5rp5pjRsBKHQYUJ0lTmh1_vyfzOzbtNN2v6W_5w9JTLrN1U6fhmifvKHppFSEqD6DameL1TC59kpIdufRkEU9HE4O-ErEf1GuJFRx-Dew6XDvb_ExhvEqcw31yNvKzpVqLYJfLazqn6tUbVuAiPwpy6rP9tYO2taT1vj5TGn_vxwDu9zoLWe796tFMPS-kmbCglxB5C9L4EbpfWNbWxYjUkTvjT2Ml9OnrB0UbYo1jI";
9pub static DEMO_USER_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwic3ViIjoiMSIsIm5iZiI6MTY3Mzc5NzgzOCwiZXhwIjo0ODI3Mzk3ODM4LCJpYXQiOjE2NzM3OTc4MzgsImp0aSI6IjJlamRwOG54a3A1djRnZWdhbWVyeWlkMW5ic24zZDhpZ2oyejgzem1vMDYzNjNoaXFkNWhwOTY1MHZwdWh4OXEybXBmd2hnbnUxNHR5cmp2In0.Ocw4CzkkuenkAOjkAR1RuFgLqix7VJ-8vWVS3KAJ1T3SgIWJG145xqG2qms99knu5azn_oaoeyMOXhyG_fuMQFGOju317GiS6pAXAFGOKvxcUCfdpFcEHO6TWGM8191-tlfV-0rAqCi62gprKyr-SrUG3nUJhv6XKegja_vYVujRVx0ouAaDvDKawiOssG5If_hXGhdhnmb3_7onnIc4hFsm4i9QVkWXe8GO6OsS999ZIX0ClNhTk2kKKTl2dDVIiKha_HB1aghm_LOYoRgb3i3B_DH4UO312rHYR5I4qO43c8x-TW7NwovItDSzhiCmcxZuUUeAUF3yFr5ovaR4fMj1LEy3y3V2piQDKPwmBOpI9S6OzWUIBJYcRYlT2HIrWCRc0YvM7AOGoxcH2Gf4ncqcF_M8fw7IMKf3pdnuxf1EbdEpzOapBD1Pw065em-U8PN4LVzw9lhIHx_Yj69qaFEx7Bhw3BCwsrx-o9hgg7T1TOV6kF11YfR99lIuj9z96XBLg5ipt-M_j7nHRoHWhM0Rc6uLIKPg0In0xYkybSfWG6v3Hs6kwgB7wkqpXpoVQltJvlqjtlf9Pp4zmkqlWQHx9as4xsgoTAQyCgaC0kisICNC58_g3QrJAfoFXW68x-OHlRKCAPqoR9V-0cVs-B83szaFmsEGegAttFLlDhE";
10
11struct ScClient {
12 client: reqwest::Client,
13 base_url: String,
14 token: String,
15}
16
17impl ScClient {
18 fn new<T: Into<String>>(base_url: T, token: String) -> ScClient {
19 ScClient {
20 client: reqwest::Client::new(),
21 base_url: base_url.into(),
22 token,
23 }
24 }
25
26 async fn request<'a, Path: Into<&'a str>, Output: DeserializeOwned>(
27 &self,
28 path: Path,
29 parameters: &mut [(String, String)],
30 ) -> Result<Output, Box<dyn std::error::Error>> {
31 let mut url = Url::parse(&self.base_url).unwrap();
32 url.set_path(path.into());
33 for (key, value) in parameters {
34 url.query_pairs_mut().append_pair(key, value);
35 }
36
37 let response = self.client.get(url)
38 .header("Authorization", format!("Bearer {}", self.token))
39 .send()
40 .await?;
41 if !response.status().is_success() {
42 return Err(format!("Request failed: {}", response.status()).into());
43 }
44 let body = response.text().await?;
45 let output: Output = serde_json::from_str(&body)?;
46 Ok(output)
47 }
48}
49
50pub async fn get_regions_list(url: &str) -> Result<Vec<Region>, Box<dyn std::error::Error>> {
51 let mut url = Url::parse(url).unwrap();
52 url.set_path("regions");
53
54 let response = reqwest::get(url)
55 .await?;
56 let body = response.text().await?;
57 let output = serde_json::from_str(&body)?;
58 Ok(output)
59}
60
61pub struct ScAppClient {
65 inner: Mutex<ScClient>,
66}
67
68impl ScAppClient {
69 pub fn new_custom<S: Into<String>>(base_url: S, user_token: impl Into<String>) -> Self {
70 Self {
71 inner: Mutex::new(ScClient::new(base_url, user_token.into()))
72 }
73 }
74 pub fn new(user_token: String) -> Self {
75 Self {
76 inner: Mutex::new(ScClient::new(BASE_URL, user_token))
77 }
78 }
79}
80
81impl ScAppClient {
82 async fn make_request<'a, Output: DeserializeOwned>(
83 &self,
84 path: &'a str,
85 parameters: &mut [(String, String)],
86 ) -> Result<Output, Box<dyn std::error::Error>> {
87 let client = self.inner.lock().await;
88 client.request(path, parameters).await
89 }
90}
91
92macro_rules! params {
93 () => {
94 Vec::<(String, String)>::new()
95 };
96 ($($name: ident : $value: expr),+) => {
97 {
98 let mut params = Vec::new();
99 $(
100 if let Some(v) = $value {
101 params.push((stringify!($name).to_string(), v.to_string()));
102 }
103 )+
104 params
105 }
106 };
107}
108macro_rules! request {
109 ($name: ident, $result: path, $pattern: expr, [$($part: ident : $t: path),*], [$($param: ident : $tt: path), *]) => {
110 pub async fn $name(&self, $($part: impl Into<$t>,)* $($param: Option<$tt>,)*) -> Result<$result, Box<dyn std::error::Error>> {
111 let mut params = params!($($param: $param),*);
112 let url = format!($pattern, $($part.into().to_string()),*);
113 self.make_request(&url, &mut params).await
114 }
115 };
116}
117
118
119impl ScAppClient {
120 request!(get_auction_price_history, crate::data::AuctionPriceHistory,
121 "{}/auction/{}/history", [region: RegionId, item_id: ItemId], [offset: u32, limit: u32]
122 );
123
124 request!(get_auction_lots, crate::data::AuctionLots,
125 "{}/auction/{}/lots", [region: RegionId, item_id: ItemId], [offset: u32, limit: u32, sort: Sort, order: Order]
126 );
127 request!(get_clan_information, crate::data::ClanInfo,
128 "{}/clan/{}/info", [region: RegionId, clan_id: ClanId], []
129 );
130 request!(get_clans_list, ClansList,
131 "{}/clans", [region: RegionId], [offset: u32, limit: u32]
132 );
133 request!(get_emission_information, EmissionInformation,
134 "{}/emission", [region: RegionId], []
135 );
136}
137
138
139pub struct ScUserClient {
143 pub app_client: ScAppClient,
144}
145
146impl ScUserClient {
147 pub fn new_custom<S: Into<String>>(base_url: S, user_token: impl Into<String>) -> ScUserClient {
148 Self {
149 app_client: ScAppClient::new_custom(base_url, user_token.into())
150 }
151 }
152 pub fn new(user_token: String) -> Self {
153 Self {
154 app_client: ScAppClient::new(user_token)
155 }
156 }
157}
158
159impl ScUserClient {
160 async fn make_request<'a, Output: DeserializeOwned>(
161 &self,
162 path: &'a str,
163 parameters: &mut [(String, String)],
164 ) -> Result<Output, Box<dyn std::error::Error>> {
165 self.app_client.make_request(path, parameters).await
166 }
167}
168
169impl ScUserClient {
170 request!(get_characters_list, Vec<Character>,
171 "{}/characters", [region: RegionId], []
172 );
173
174 request!(get_clan_members, Vec<ClanMember>,
175 "{}/clan/{}/members", [region: RegionId, clan_id: ClanId], []
176 );
177 request!(get_list_of_friends, Vec<String>,
178 "{}/friends/{}", [region: RegionId, character_id: CharacterId], []
179 );
180}
181
182
183#[cfg(test)]
184mod test {
185 use super::*;
186
187 fn demo_user() -> ScUserClient {
188 ScUserClient::new_custom(DEMO_URL, DEMO_USER_TOKEN)
189 }
190
191 fn demo_app() -> ScAppClient {
192 ScAppClient::new_custom(DEMO_URL, DEMO_APP_TOKEN)
193 }
194
195 #[tokio::test]
196 async fn regions_list() {
197 let regions = get_regions_list(DEMO_URL).await.unwrap();
198 assert!(!regions.is_empty());
199 }
200
201 #[tokio::test]
202 async fn auction_price_history() {
203 let client = demo_app();
204 let history = client.get_auction_price_history("RU", "3grl", None, None).await.unwrap();
205 println!("{:?}", history);
206 }
207
208 #[tokio::test]
209 async fn auction_lots() {
210 let client = demo_app();
211 let history = client.get_auction_lots("RU", "3grl", None, None, None, None).await.unwrap();
212 println!("{:?}", history);
213 }
214
215 #[tokio::test]
216 async fn clan_info() {
217 let client = demo_app();
218 let info = client.get_clan_information("RU", "647d6c53-b3d7-4d30-8d08-de874eb1d845").await.unwrap();
219 println!("{:?}", info);
220 }
221
222 #[tokio::test]
223 async fn characters_list() {
224 let client = demo_user();
225 let info = client.get_characters_list("RU").await.unwrap();
226 println!("{:?}", info);
227 }
228
229 #[tokio::test]
230 async fn clan_members() {
231 let client = demo_user();
232 let info = client.get_clan_members("RU", "647d6c53-b3d7-4d30-8d08-de874eb1d845").await.unwrap();
233 println!("{:?}", info);
234 }
235
236 #[tokio::test]
237 async fn clans_list() {
238 let client = demo_app();
239 let info = client.get_clans_list("RU", None, None).await.unwrap();
240 println!("{:?}", info);
241 }
242
243 #[tokio::test]
244 async fn clans_list_off_1() {
245 let client = demo_app();
246 let info = client.get_clans_list("RU", Some(1), None).await.unwrap();
247 println!("{:?}", info);
248 }
249
250 #[tokio::test]
251 async fn clans_list_lim_1() {
252 let client = demo_app();
253 let info = client.get_clans_list("RU", None, Some(1)).await.unwrap();
254 assert_eq!(info.data.len(), 1);
255 println!("{:?}", info);
256 }
257
258 #[tokio::test]
259 async fn emission_info() {
260 let client = demo_app();
261 let info = client.get_emission_information("RU").await.unwrap();
262 println!("{:?}", info);
263 }
264
265 #[tokio::test]
266 async fn friends_list() {
267 let client = demo_user();
268 let info = client.get_list_of_friends("RU", "Test-1").await.unwrap();
269 println!("{:?}", info);
270 }
271}