1use crate::api::error::EpicAPIError;
2use crate::api::types::download_manifest::DownloadManifest;
3use crate::api::types::fab_asset_manifest::DownloadInfo;
4use crate::api::types::fab_library::FabLibrary;
5use crate::api::EpicAPI;
6use log::{debug, error, warn};
7use std::borrow::BorrowMut;
8use url::Url;
9
10impl EpicAPI {
11 pub async fn fab_asset_manifest(
13 &self,
14 artifact_id: &str,
15 namespace: &str,
16 asset_id: &str,
17 platform: Option<&str>,
18 ) -> Result<Vec<DownloadInfo>, EpicAPIError> {
19 let url = format!("https://www.fab.com/e/artifacts/{}/manifest", artifact_id);
20 let parsed_url = Url::parse(&url).map_err(|_| EpicAPIError::InvalidParams)?;
21 match self
22 .authorized_post_client(parsed_url)
23 .json(&serde_json::json!({
24 "item_id": asset_id,
25 "namespace": namespace,
26 "platform": platform.unwrap_or("Windows"),
27 }))
28 .send()
29 .await
30 {
31 Ok(response) => {
32 if response.status() == reqwest::StatusCode::OK {
33 let text = response.text().await.unwrap_or_default();
34 match serde_json::from_str::<
35 crate::api::types::fab_asset_manifest::FabAssetManifest,
36 >(&text)
37 {
38 Ok(manifest) => Ok(manifest.download_info),
39 Err(e) => {
40 error!("{:?}", e);
41 debug!("{}", text);
42 Err(EpicAPIError::DeserializationError(format!("{}", e)))
43 }
44 }
45 } else if response.status() == reqwest::StatusCode::FORBIDDEN {
46 Err(EpicAPIError::FabTimeout)
47 } else {
48 debug!("{:?}", response.headers());
49 let status = response.status();
50 let body = response.text().await.unwrap_or_default();
51 warn!("{} result: {}", status, body);
52 Err(EpicAPIError::HttpError { status, body })
53 }
54 }
55 Err(e) => {
56 error!("{:?}", e);
57 Err(EpicAPIError::NetworkError(e))
58 }
59 }
60 }
61
62 pub async fn fab_download_manifest(
64 &self,
65 download_info: DownloadInfo,
66 distribution_point_url: &str,
67 ) -> Result<DownloadManifest, EpicAPIError> {
68 match download_info.get_distribution_point_by_base_url(distribution_point_url) {
69 None => {
70 error!("Distribution point not found");
71 Err(EpicAPIError::InvalidParams)
72 }
73 Some(point) => {
74 if point.signature_expiration < time::OffsetDateTime::now_utc() {
75 error!("Expired signature");
76 Err(EpicAPIError::InvalidParams)
77 } else {
78 let data = self.get_bytes(&point.manifest_url).await?;
79 match DownloadManifest::parse(data) {
80 None => {
81 error!("Unable to parse the Download Manifest");
82 Err(EpicAPIError::DeserializationError(
83 "Unable to parse the Download Manifest".to_string(),
84 ))
85 }
86 Some(mut man) => {
87 man.set_custom_field("SourceURL", distribution_point_url);
88 Ok(man)
89 }
90 }
91 }
92 }
93 }
94 }
95
96 pub async fn fab_library_items(
98 &mut self,
99 account_id: String,
100 ) -> Result<FabLibrary, EpicAPIError> {
101 let mut library = FabLibrary::default();
102
103 loop {
104 let url = match &library.cursors.next {
105 None => {
106 format!(
107 "https://www.fab.com/e/accounts/{}/ue/library?count=100",
108 account_id
109 )
110 }
111 Some(c) => {
112 format!(
113 "https://www.fab.com/e/accounts/{}/ue/library?cursor={}&count=100",
114 account_id, c
115 )
116 }
117 };
118
119 match self.authorized_get_json::<FabLibrary>(&url).await {
120 Ok(mut api_library) => {
121 library.cursors.next = api_library.cursors.next;
122 library.results.append(api_library.results.borrow_mut());
123 }
124 Err(e) => {
125 error!("{:?}", e);
126 library.cursors.next = None;
127 }
128 }
129 if library.cursors.next.is_none() {
130 break;
131 }
132 }
133
134 Ok(library)
135 }
136
137 pub async fn fab_file_download_info(
139 &self,
140 listing_id: &str,
141 format_id: &str,
142 file_id: &str,
143 ) -> Result<DownloadInfo, EpicAPIError> {
144 let url = format!(
145 "https://www.fab.com/p/egl/listings/{}/asset-formats/{}/files/{}/download-info",
146 listing_id, format_id, file_id
147 );
148 self.authorized_get_json(&url).await
149 }
150
151 pub async fn fab_search(
155 &self,
156 params: &crate::api::types::fab_search::FabSearchParams,
157 ) -> Result<crate::api::types::fab_search::FabSearchResults, EpicAPIError> {
158 let mut url = "https://www.fab.com/i/listings/search?".to_string();
159 let mut query_parts = Vec::new();
160
161 if let Some(ref q) = params.q {
162 query_parts.push(format!("q={}", q));
163 }
164 if let Some(ref channels) = params.channels {
165 query_parts.push(format!("channels={}", channels));
166 }
167 if let Some(ref listing_types) = params.listing_types {
168 query_parts.push(format!("listing_types={}", listing_types));
169 }
170 if let Some(ref categories) = params.categories {
171 query_parts.push(format!("categories={}", categories));
172 }
173 if let Some(ref sort_by) = params.sort_by {
174 query_parts.push(format!("sort_by={}", sort_by));
175 }
176 if let Some(count) = params.count {
177 query_parts.push(format!("count={}", count));
178 }
179 if let Some(ref cursor) = params.cursor {
180 query_parts.push(format!("cursor={}", cursor));
181 }
182 if let Some(ref aggregate_on) = params.aggregate_on {
183 query_parts.push(format!("aggregate_on={}", aggregate_on));
184 }
185 if let Some(ref in_filter) = params.in_filter {
186 query_parts.push(format!("in={}", in_filter));
187 }
188 if let Some(is_discounted) = params.is_discounted {
189 if is_discounted {
190 query_parts.push("is_discounted=true".to_string());
191 }
192 }
193 if let Some(is_free) = params.is_free {
194 if is_free {
195 query_parts.push("is_free=1".to_string());
196 }
197 }
198 if let Some(pct) = params.min_discount_percentage {
199 query_parts.push(format!("min_discount_percentage={}", pct));
200 }
201 if let Some(ref seller) = params.seller {
202 query_parts.push(format!("seller={}", seller));
203 }
204
205 url.push_str(&query_parts.join("&"));
206 self.get_json(&url).await
207 }
208
209 pub async fn fab_listing(
211 &self,
212 uid: &str,
213 ) -> Result<crate::api::types::fab_search::FabListingDetail, EpicAPIError> {
214 let url = format!("https://www.fab.com/i/listings/{}", uid);
215 self.get_json(&url).await
216 }
217
218 pub async fn fab_listing_ue_formats(
220 &self,
221 uid: &str,
222 ) -> Result<Vec<crate::api::types::fab_search::FabListingUeFormat>, EpicAPIError> {
223 let url = format!(
224 "https://www.fab.com/i/listings/{}/asset-formats/unreal-engine",
225 uid
226 );
227 self.get_json(&url).await
228 }
229
230 pub async fn fab_listing_state(
232 &self,
233 uid: &str,
234 ) -> Result<crate::api::types::fab_search::FabListingState, EpicAPIError> {
235 let url = format!("https://www.fab.com/i/users/me/listings-states/{}", uid);
236 self.authorized_get_json(&url).await
237 }
238
239 pub async fn fab_listing_states_bulk(
241 &self,
242 listing_ids: &[&str],
243 ) -> Result<Vec<crate::api::types::fab_search::FabListingState>, EpicAPIError> {
244 let ids = listing_ids.join(",");
245 let url = format!(
246 "https://www.fab.com/i/users/me/listings-states?listing_ids={}",
247 ids
248 );
249 self.authorized_get_json(&url).await
250 }
251
252 pub async fn fab_bulk_prices(
254 &self,
255 offer_ids: &[&str],
256 ) -> Result<crate::api::types::fab_search::FabBulkPricesResponse, EpicAPIError> {
257 let ids = offer_ids
258 .iter()
259 .map(|id| format!("offer_ids={}", id))
260 .collect::<Vec<_>>()
261 .join("&");
262 let url = format!("https://www.fab.com/i/listings/prices-infos?{}", ids);
263 self.get_json(&url).await
264 }
265
266 pub async fn fab_listing_ownership(
268 &self,
269 uid: &str,
270 ) -> Result<crate::api::types::fab_search::FabOwnership, EpicAPIError> {
271 let url = format!("https://www.fab.com/i/listings/{}/ownership", uid);
272 self.authorized_get_json(&url).await
273 }
274
275 pub async fn fab_listing_prices(
277 &self,
278 uid: &str,
279 ) -> Result<Vec<crate::api::types::fab_search::FabPriceInfo>, EpicAPIError> {
280 let url = format!("https://www.fab.com/i/listings/{}/prices-infos", uid);
281 self.get_json(&url).await
282 }
283
284 pub async fn fab_listing_reviews(
286 &self,
287 uid: &str,
288 sort_by: Option<&str>,
289 cursor: Option<&str>,
290 ) -> Result<crate::api::types::fab_search::FabReviewsResponse, EpicAPIError> {
291 let mut query_parts = Vec::new();
292 if let Some(sort) = sort_by {
293 query_parts.push(format!("sort_by={}", sort));
294 }
295 if let Some(c) = cursor {
296 query_parts.push(format!("cursor={}", c));
297 }
298 let url = if query_parts.is_empty() {
299 format!("https://www.fab.com/i/store/listings/{}/reviews", uid)
300 } else {
301 format!(
302 "https://www.fab.com/i/store/listings/{}/reviews?{}",
303 uid,
304 query_parts.join("&")
305 )
306 };
307 self.get_json(&url).await
308 }
309
310 pub async fn fab_licenses(
312 &self,
313 ) -> Result<Vec<crate::api::types::fab_taxonomy::FabLicenseType>, EpicAPIError> {
314 self.get_json("https://www.fab.com/i/taxonomy/licenses").await
315 }
316
317 pub async fn fab_format_groups(
319 &self,
320 ) -> Result<Vec<crate::api::types::fab_taxonomy::FabFormatGroup>, EpicAPIError> {
321 self.get_json("https://www.fab.com/i/taxonomy/asset-format-groups").await
322 }
323
324 pub async fn fab_tag_groups(
326 &self,
327 ) -> Result<Vec<crate::api::types::fab_taxonomy::FabTagGroup>, EpicAPIError> {
328 let wrapper: crate::api::types::fab_taxonomy::FabResultsWrapper<
329 crate::api::types::fab_taxonomy::FabTagGroup,
330 > = self.get_json("https://www.fab.com/i/tags/groups").await?;
331 Ok(wrapper.results)
332 }
333
334 pub async fn fab_ue_versions(
336 &self,
337 ) -> Result<Vec<String>, EpicAPIError> {
338 self.get_json("https://www.fab.com/i/unreal-engine/versions").await
339 }
340
341 pub async fn fab_channel(
343 &self,
344 slug: &str,
345 ) -> Result<crate::api::types::fab_taxonomy::FabChannel, EpicAPIError> {
346 let url = format!("https://www.fab.com/i/channels/{}", slug);
347 self.get_json(&url).await
348 }
349
350 pub async fn fab_library_entitlements(
353 &self,
354 params: &crate::api::types::fab_entitlement::FabEntitlementSearchParams,
355 ) -> Result<crate::api::types::fab_entitlement::FabEntitlementResults, EpicAPIError> {
356 let mut query_parts = Vec::new();
357 if let Some(ref sort_by) = params.sort_by {
358 query_parts.push(format!("sort_by={}", sort_by));
359 }
360 if let Some(ref cursor) = params.cursor {
361 query_parts.push(format!("cursor={}", cursor));
362 }
363 if let Some(ref listing_types) = params.listing_types {
364 query_parts.push(format!("listing_types={}", listing_types));
365 }
366 if let Some(ref categories) = params.categories {
367 query_parts.push(format!("categories={}", categories));
368 }
369 if let Some(ref tags) = params.tags {
370 query_parts.push(format!("tags={}", tags));
371 }
372 if let Some(ref licenses) = params.licenses {
373 query_parts.push(format!("licenses={}", licenses));
374 }
375 if let Some(ref asset_formats) = params.asset_formats {
376 query_parts.push(format!("asset_formats={}", asset_formats));
377 }
378 if let Some(ref source) = params.source {
379 query_parts.push(format!("source={}", source));
380 }
381 if let Some(ref aggregate_on) = params.aggregate_on {
382 query_parts.push(format!("aggregate_on={}", aggregate_on));
383 }
384 if let Some(count) = params.count {
385 query_parts.push(format!("count={}", count));
386 }
387 if let Some(ref added_since) = params.added_since {
388 query_parts.push(format!("added_since={}", added_since));
389 }
390
391 let url = if query_parts.is_empty() {
392 "https://www.fab.com/i/library/entitlements/search".to_string()
393 } else {
394 format!(
395 "https://www.fab.com/i/library/entitlements/search?{}",
396 query_parts.join("&")
397 )
398 };
399 self.authorized_get_json(&url).await
400 }
401
402 pub async fn fab_csrf(&self) -> Result<(), EpicAPIError> {
404 let parsed_url =
405 url::Url::parse("https://www.fab.com/i/csrf").map_err(|_| EpicAPIError::InvalidParams)?;
406 let response = self.client.get(parsed_url).send().await.map_err(|e| {
407 error!("{:?}", e);
408 EpicAPIError::NetworkError(e)
409 })?;
410 if response.status().is_success() {
411 Ok(())
412 } else {
413 let status = response.status();
414 let body = response.text().await.unwrap_or_default();
415 warn!("{} result: {}", status, body);
416 Err(EpicAPIError::HttpError { status, body })
417 }
418 }
419
420 pub async fn fab_user_context(
422 &self,
423 ) -> Result<crate::api::types::fab_search::FabUserContext, EpicAPIError> {
424 self.get_json("https://www.fab.com/i/users/context").await
425 }
426
427 pub async fn fab_add_to_library(&self, listing_uid: &str) -> Result<(), EpicAPIError> {
429 let url = format!(
430 "https://www.fab.com/i/listings/{}/add-to-library",
431 listing_uid
432 );
433 let parsed_url = Url::parse(&url).map_err(|_| EpicAPIError::InvalidParams)?;
434 let response = self
435 .authorized_post_client(parsed_url)
436 .send()
437 .await
438 .map_err(|e| {
439 error!("{:?}", e);
440 EpicAPIError::NetworkError(e)
441 })?;
442 if response.status() == reqwest::StatusCode::NO_CONTENT
443 || response.status() == reqwest::StatusCode::OK
444 {
445 Ok(())
446 } else {
447 let status = response.status();
448 let body = response.text().await.unwrap_or_default();
449 warn!("{} result: {}", status, body);
450 Err(EpicAPIError::HttpError { status, body })
451 }
452 }
453
454 pub async fn fab_listing_formats(
456 &self,
457 listing_uid: &str,
458 ) -> Result<Vec<crate::api::types::fab_search::FabListingFormat>, EpicAPIError> {
459 let url = format!(
460 "https://www.fab.com/i/listings/{}/asset-formats",
461 listing_uid
462 );
463 self.authorized_get_json(&url).await
464 }
465}