1use crate::api::error::EpicAPIError;
2use crate::api::types::cosmos::{
3 CosmosAccount, CosmosAuthResponse, CosmosCommOptIn, CosmosEulaResponse, CosmosSearchResults,
4 CosmosPolicyAodc,
5};
6use crate::api::types::engine_blob::EngineBlobsResponse;
7use crate::api::EpicAPI;
8use log::{debug, error, warn};
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize)]
12#[serde(rename_all = "camelCase")]
13struct RedirectResponse {
14 #[allow(dead_code)]
15 redirect_url: Option<String>,
16 #[allow(dead_code)]
17 authorization_code: Option<serde_json::Value>,
18 sid: Option<String>,
19}
20
21impl EpicAPI {
22 pub async fn cosmos_session_setup(
34 &self,
35 exchange_code: &str,
36 ) -> Result<CosmosAuthResponse, EpicAPIError> {
37 let rep_response = self
38 .client
39 .get("https://www.epicgames.com/id/api/reputation")
40 .send()
41 .await
42 .map_err(|e| {
43 error!("Failed to get XSRF token: {:?}", e);
44 EpicAPIError::NetworkError(e)
45 })?;
46
47 let xsrf = rep_response
48 .cookies()
49 .find(|c| c.name() == "XSRF-TOKEN")
50 .map(|c| c.value().to_string())
51 .ok_or_else(|| {
52 error!("XSRF-TOKEN cookie not found in reputation response");
53 EpicAPIError::InvalidCredentials
54 })?;
55
56 let exchange_body = serde_json::json!({"exchangeCode": exchange_code});
57 let exchange_resp = self
58 .client
59 .post("https://www.epicgames.com/id/api/exchange")
60 .json(&exchange_body)
61 .header("x-xsrf-token", &xsrf)
62 .send()
63 .await
64 .map_err(|e| {
65 error!("Failed exchange code: {:?}", e);
66 EpicAPIError::NetworkError(e)
67 })?;
68
69 if !exchange_resp.status().is_success() {
70 let status = exchange_resp.status();
71 let body = exchange_resp.text().await.unwrap_or_default();
72 warn!("Exchange failed: {} {}", status, body);
73 return Err(EpicAPIError::HttpError { status, body });
74 }
75
76 let redirect_resp = self
77 .client
78 .get("https://www.epicgames.com/id/api/redirect?")
79 .send()
80 .await
81 .map_err(|e| {
82 error!("Failed redirect: {:?}", e);
83 EpicAPIError::NetworkError(e)
84 })?;
85
86 let redirect: RedirectResponse = redirect_resp.json().await.map_err(|e| {
87 error!("Failed to parse redirect response: {:?}", e);
88 EpicAPIError::DeserializationError(format!("{}", e))
89 })?;
90
91 let sid = redirect.sid.ok_or_else(|| {
92 error!("No SID in redirect response");
93 EpicAPIError::InvalidCredentials
94 })?;
95
96 let set_sid_resp = self
97 .client
98 .get(format!(
99 "https://www.unrealengine.com/id/api/set-sid?sid={}",
100 sid
101 ))
102 .send()
103 .await
104 .map_err(|e| {
105 error!("Failed set-sid: {:?}", e);
106 EpicAPIError::NetworkError(e)
107 })?;
108 debug!("set-sid status={}", set_sid_resp.status());
109
110 self.cosmos_auth_upgrade().await
111 }
112
113 pub async fn cosmos_auth_upgrade(&self) -> Result<CosmosAuthResponse, EpicAPIError> {
117 let response = self
118 .client
119 .get("https://www.unrealengine.com/api/cosmos/auth")
120 .header("Accept", "application/json")
121 .send()
122 .await
123 .map_err(|e| {
124 error!("Failed cosmos auth: {:?}", e);
125 EpicAPIError::NetworkError(e)
126 })?;
127
128 if response.status().is_success() {
129 response.json::<CosmosAuthResponse>().await.map_err(|e| {
130 error!("Failed to parse cosmos auth response: {:?}", e);
131 EpicAPIError::DeserializationError(format!("{}", e))
132 })
133 } else {
134 let status = response.status();
135 let body = response.text().await.unwrap_or_default();
136 warn!("cosmos/auth failed: {} {}", status, body);
137 Err(EpicAPIError::HttpError { status, body })
138 }
139 }
140
141 pub async fn cosmos_eula_check(
146 &self,
147 eula_id: &str,
148 locale: &str,
149 ) -> Result<CosmosEulaResponse, EpicAPIError> {
150 let url = format!(
151 "https://www.unrealengine.com/api/cosmos/eula/accept?eulaId={}&locale={}",
152 eula_id, locale
153 );
154 let response = self
155 .client
156 .get(&url)
157 .header("Accept", "application/json")
158 .send()
159 .await
160 .map_err(|e| {
161 error!("Failed EULA check: {:?}", e);
162 EpicAPIError::NetworkError(e)
163 })?;
164
165 if response.status().is_success() {
166 response.json::<CosmosEulaResponse>().await.map_err(|e| {
167 error!("Failed to parse EULA response: {:?}", e);
168 EpicAPIError::DeserializationError(format!("{}", e))
169 })
170 } else {
171 let status = response.status();
172 let body = response.text().await.unwrap_or_default();
173 warn!("EULA check failed: {} {}", status, body);
174 Err(EpicAPIError::HttpError { status, body })
175 }
176 }
177
178 pub async fn cosmos_eula_accept(
183 &self,
184 eula_id: &str,
185 locale: &str,
186 version: u32,
187 ) -> Result<CosmosEulaResponse, EpicAPIError> {
188 let url = format!(
189 "https://www.unrealengine.com/api/cosmos/eula/accept?eulaId={}&locale={}&version={}",
190 eula_id, locale, version
191 );
192 let response = self
193 .client
194 .post(&url)
195 .header("Accept", "application/json")
196 .send()
197 .await
198 .map_err(|e| {
199 error!("Failed EULA accept: {:?}", e);
200 EpicAPIError::NetworkError(e)
201 })?;
202
203 if response.status().is_success() {
204 response.json::<CosmosEulaResponse>().await.map_err(|e| {
205 error!("Failed to parse EULA accept response: {:?}", e);
206 EpicAPIError::DeserializationError(format!("{}", e))
207 })
208 } else {
209 let status = response.status();
210 let body = response.text().await.unwrap_or_default();
211 warn!("EULA accept failed: {} {}", status, body);
212 Err(EpicAPIError::HttpError { status, body })
213 }
214 }
215
216 pub async fn cosmos_account(&self) -> Result<CosmosAccount, EpicAPIError> {
218 let response = self
219 .client
220 .get("https://www.unrealengine.com/api/cosmos/account")
221 .header("Accept", "application/json")
222 .send()
223 .await
224 .map_err(|e| {
225 error!("Failed cosmos account: {:?}", e);
226 EpicAPIError::NetworkError(e)
227 })?;
228
229 if response.status().is_success() {
230 response.json::<CosmosAccount>().await.map_err(|e| {
231 error!("Failed to parse cosmos account response: {:?}", e);
232 EpicAPIError::DeserializationError(format!("{}", e))
233 })
234 } else {
235 let status = response.status();
236 let body = response.text().await.unwrap_or_default();
237 warn!("cosmos/account failed: {} {}", status, body);
238 Err(EpicAPIError::HttpError { status, body })
239 }
240 }
241
242 pub async fn cosmos_policy_aodc(&self) -> Result<CosmosPolicyAodc, EpicAPIError> {
244 let response = self
245 .client
246 .get("https://www.unrealengine.com/api/cosmos/policy/aodc")
247 .header("Accept", "application/json")
248 .send()
249 .await
250 .map_err(|e| {
251 error!("Failed cosmos policy check: {:?}", e);
252 EpicAPIError::NetworkError(e)
253 })?;
254
255 if response.status().is_success() {
256 response.json::<CosmosPolicyAodc>().await.map_err(|e| {
257 error!("Failed to parse policy response: {:?}", e);
258 EpicAPIError::DeserializationError(format!("{}", e))
259 })
260 } else {
261 let status = response.status();
262 let body = response.text().await.unwrap_or_default();
263 warn!("cosmos/policy/aodc failed: {} {}", status, body);
264 Err(EpicAPIError::HttpError { status, body })
265 }
266 }
267
268 pub async fn cosmos_comm_opt_in(
273 &self,
274 setting: &str,
275 ) -> Result<CosmosCommOptIn, EpicAPIError> {
276 let url = format!(
277 "https://www.unrealengine.com/api/cosmos/communication/opt-in?setting={}",
278 setting
279 );
280 let response = self
281 .client
282 .get(&url)
283 .header("Accept", "application/json")
284 .send()
285 .await
286 .map_err(|e| {
287 error!("Failed cosmos comm opt-in: {:?}", e);
288 EpicAPIError::NetworkError(e)
289 })?;
290
291 if response.status().is_success() {
292 response.json::<CosmosCommOptIn>().await.map_err(|e| {
293 error!("Failed to parse comm opt-in response: {:?}", e);
294 EpicAPIError::DeserializationError(format!("{}", e))
295 })
296 } else {
297 let status = response.status();
298 let body = response.text().await.unwrap_or_default();
299 warn!("cosmos/communication/opt-in failed: {} {}", status, body);
300 Err(EpicAPIError::HttpError { status, body })
301 }
302 }
303
304 pub async fn engine_versions(
309 &self,
310 platform: &str,
311 ) -> Result<EngineBlobsResponse, EpicAPIError> {
312 let url = format!("https://www.unrealengine.com/api/blobs/{}", platform);
313 let response = self
314 .client
315 .get(&url)
316 .header("Accept", "application/json")
317 .send()
318 .await
319 .map_err(|e| {
320 error!("Failed engine versions: {:?}", e);
321 EpicAPIError::NetworkError(e)
322 })?;
323
324 if response.status().is_success() {
325 response.json::<EngineBlobsResponse>().await.map_err(|e| {
326 error!("Failed to parse engine versions response: {:?}", e);
327 EpicAPIError::DeserializationError(format!("{}", e))
328 })
329 } else {
330 let status = response.status();
331 let body = response.text().await.unwrap_or_default();
332 warn!("blobs/{} failed: {} {}", platform, status, body);
333 Err(EpicAPIError::HttpError { status, body })
334 }
335 }
336
337 pub async fn cosmos_search(
339 &self,
340 query: &str,
341 slug: Option<&str>,
342 locale: Option<&str>,
343 filter: Option<&str>,
344 ) -> Result<CosmosSearchResults, EpicAPIError> {
345 let mut url = format!(
346 "https://www.unrealengine.com/api/cosmos/search?query={}",
347 query
348 );
349 if let Some(s) = slug {
350 url.push_str(&format!("&slug={}", s));
351 }
352 if let Some(l) = locale {
353 url.push_str(&format!("&locale={}", l));
354 }
355 if let Some(f) = filter {
356 url.push_str(&format!("&filter={}", f));
357 }
358 let response = self
359 .client
360 .get(&url)
361 .header("Accept", "application/json")
362 .send()
363 .await
364 .map_err(|e| {
365 error!("Failed cosmos search: {:?}", e);
366 EpicAPIError::NetworkError(e)
367 })?;
368
369 if response.status().is_success() {
370 response
371 .json::<CosmosSearchResults>()
372 .await
373 .map_err(|e| {
374 error!("Failed to parse cosmos search response: {:?}", e);
375 EpicAPIError::DeserializationError(format!("{}", e))
376 })
377 } else {
378 let status = response.status();
379 let body = response.text().await.unwrap_or_default();
380 warn!("cosmos/search failed: {} {}", status, body);
381 Err(EpicAPIError::HttpError { status, body })
382 }
383 }
384}