sc_api/
client.rs

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
61/**
62 * Client for requests that do not require user authentication.
63 */
64pub 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
139/**
140 * Client for requests that do require user authentication.
141 */
142pub 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}