1use crate::api::EpicAPI;
2use crate::api::error::EpicAPIError;
3use crate::api::types::cosmos::{
4 CosmosAccount, CosmosAuthResponse, CosmosCommOptIn, CosmosEulaResponse, CosmosPolicyAodc,
5 CosmosSearchResults,
6};
7use crate::api::types::engine_blob::EngineBlobsResponse;
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(&self, setting: &str) -> Result<CosmosCommOptIn, EpicAPIError> {
273 let url = format!(
274 "https://www.unrealengine.com/api/cosmos/communication/opt-in?setting={}",
275 setting
276 );
277 let response = self
278 .client
279 .get(&url)
280 .header("Accept", "application/json")
281 .send()
282 .await
283 .map_err(|e| {
284 error!("Failed cosmos comm opt-in: {:?}", e);
285 EpicAPIError::NetworkError(e)
286 })?;
287
288 if response.status().is_success() {
289 response.json::<CosmosCommOptIn>().await.map_err(|e| {
290 error!("Failed to parse comm opt-in response: {:?}", e);
291 EpicAPIError::DeserializationError(format!("{}", e))
292 })
293 } else {
294 let status = response.status();
295 let body = response.text().await.unwrap_or_default();
296 warn!("cosmos/communication/opt-in failed: {} {}", status, body);
297 Err(EpicAPIError::HttpError { status, body })
298 }
299 }
300
301 pub async fn engine_versions(
306 &self,
307 platform: &str,
308 ) -> Result<EngineBlobsResponse, EpicAPIError> {
309 let url = format!("https://www.unrealengine.com/api/blobs/{}", platform);
310 let response = self
311 .client
312 .get(&url)
313 .header("Accept", "application/json")
314 .send()
315 .await
316 .map_err(|e| {
317 error!("Failed engine versions: {:?}", e);
318 EpicAPIError::NetworkError(e)
319 })?;
320
321 if response.status().is_success() {
322 response.json::<EngineBlobsResponse>().await.map_err(|e| {
323 error!("Failed to parse engine versions response: {:?}", e);
324 EpicAPIError::DeserializationError(format!("{}", e))
325 })
326 } else {
327 let status = response.status();
328 let body = response.text().await.unwrap_or_default();
329 warn!("blobs/{} failed: {} {}", platform, status, body);
330 Err(EpicAPIError::HttpError { status, body })
331 }
332 }
333
334 pub async fn cosmos_search(
336 &self,
337 query: &str,
338 slug: Option<&str>,
339 locale: Option<&str>,
340 filter: Option<&str>,
341 ) -> Result<CosmosSearchResults, EpicAPIError> {
342 let mut url = format!(
343 "https://www.unrealengine.com/api/cosmos/search?query={}",
344 query
345 );
346 if let Some(s) = slug {
347 url.push_str(&format!("&slug={}", s));
348 }
349 if let Some(l) = locale {
350 url.push_str(&format!("&locale={}", l));
351 }
352 if let Some(f) = filter {
353 url.push_str(&format!("&filter={}", f));
354 }
355 let response = self
356 .client
357 .get(&url)
358 .header("Accept", "application/json")
359 .send()
360 .await
361 .map_err(|e| {
362 error!("Failed cosmos search: {:?}", e);
363 EpicAPIError::NetworkError(e)
364 })?;
365
366 if response.status().is_success() {
367 response.json::<CosmosSearchResults>().await.map_err(|e| {
368 error!("Failed to parse cosmos search response: {:?}", e);
369 EpicAPIError::DeserializationError(format!("{}", e))
370 })
371 } else {
372 let status = response.status();
373 let body = response.text().await.unwrap_or_default();
374 warn!("cosmos/search failed: {} {}", status, body);
375 Err(EpicAPIError::HttpError { status, body })
376 }
377 }
378}