Skip to main content

egs_api/api/
fab.rs

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    /// Fetch Fab asset manifest with signed distribution points. Returns `FabTimeout` on 403.
12    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    /// Download and parse a Fab manifest from a distribution point.
63    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    /// Fetch all Fab library items, paginating internally.
97    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    /// Fetch download info for a specific file within a Fab listing.
138    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    /// Search Fab listings. Public endpoint — no auth required.
152    ///
153    /// Use `FabSearchParams` to specify filters, sorting, and pagination.
154    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
194        url.push_str(&query_parts.join("&"));
195        self.get_json(&url).await
196    }
197
198    /// Get full listing detail. Public endpoint — no auth required.
199    pub async fn fab_listing(
200        &self,
201        uid: &str,
202    ) -> Result<crate::api::types::fab_search::FabListingDetail, EpicAPIError> {
203        let url = format!("https://www.fab.com/i/listings/{}", uid);
204        self.get_json(&url).await
205    }
206
207    /// Get UE-specific format details for a listing. Public endpoint.
208    pub async fn fab_listing_ue_formats(
209        &self,
210        uid: &str,
211    ) -> Result<Vec<crate::api::types::fab_search::FabListingUeFormat>, EpicAPIError> {
212        let url = format!(
213            "https://www.fab.com/i/listings/{}/asset-formats/unreal-engine",
214            uid
215        );
216        self.get_json(&url).await
217    }
218
219    /// Get user's listing state (ownership, wishlist, review). Requires Fab session.
220    pub async fn fab_listing_state(
221        &self,
222        uid: &str,
223    ) -> Result<crate::api::types::fab_search::FabListingState, EpicAPIError> {
224        let url = format!("https://www.fab.com/i/users/me/listings-states/{}", uid);
225        self.authorized_get_json(&url).await
226    }
227
228    /// Bulk check listing states for multiple IDs. Requires Fab session.
229    pub async fn fab_listing_states_bulk(
230        &self,
231        listing_ids: &[&str],
232    ) -> Result<Vec<crate::api::types::fab_search::FabListingState>, EpicAPIError> {
233        let ids = listing_ids.join(",");
234        let url = format!(
235            "https://www.fab.com/i/users/me/listings-states?listing_ids={}",
236            ids
237        );
238        self.authorized_get_json(&url).await
239    }
240
241    /// Bulk fetch pricing for multiple offer IDs. Public endpoint.
242    pub async fn fab_bulk_prices(
243        &self,
244        offer_ids: &[&str],
245    ) -> Result<Vec<crate::api::types::fab_search::FabPriceInfo>, EpicAPIError> {
246        let ids = offer_ids
247            .iter()
248            .map(|id| format!("offer_ids={}", id))
249            .collect::<Vec<_>>()
250            .join("&");
251        let url = format!("https://www.fab.com/i/listings/prices-infos?{}", ids);
252        self.get_json(&url).await
253    }
254
255    /// Get listing ownership info. Requires Fab session.
256    pub async fn fab_listing_ownership(
257        &self,
258        uid: &str,
259    ) -> Result<crate::api::types::fab_search::FabOwnership, EpicAPIError> {
260        let url = format!("https://www.fab.com/i/listings/{}/ownership", uid);
261        self.authorized_get_json(&url).await
262    }
263
264    /// Get pricing for a specific listing. Public endpoint.
265    pub async fn fab_listing_prices(
266        &self,
267        uid: &str,
268    ) -> Result<Vec<crate::api::types::fab_search::FabPriceInfo>, EpicAPIError> {
269        let url = format!("https://www.fab.com/i/listings/{}/prices-infos", uid);
270        self.get_json(&url).await
271    }
272
273    /// Get reviews for a listing. Public endpoint.
274    pub async fn fab_listing_reviews(
275        &self,
276        uid: &str,
277        sort_by: Option<&str>,
278        cursor: Option<&str>,
279    ) -> Result<crate::api::types::fab_search::FabReviewsResponse, EpicAPIError> {
280        let mut query_parts = Vec::new();
281        if let Some(sort) = sort_by {
282            query_parts.push(format!("sort_by={}", sort));
283        }
284        if let Some(c) = cursor {
285            query_parts.push(format!("cursor={}", c));
286        }
287        let url = if query_parts.is_empty() {
288            format!("https://www.fab.com/i/store/listings/{}/reviews", uid)
289        } else {
290            format!(
291                "https://www.fab.com/i/store/listings/{}/reviews?{}",
292                uid,
293                query_parts.join("&")
294            )
295        };
296        self.get_json(&url).await
297    }
298
299    /// Fetch available license types. Public endpoint.
300    pub async fn fab_licenses(
301        &self,
302    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabLicenseType>, EpicAPIError> {
303        self.get_json("https://www.fab.com/i/taxonomy/licenses").await
304    }
305
306    /// Fetch asset format groups. Public endpoint.
307    pub async fn fab_format_groups(
308        &self,
309    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabFormatGroup>, EpicAPIError> {
310        self.get_json("https://www.fab.com/i/taxonomy/asset-format-groups").await
311    }
312
313    /// Fetch tag groups with nested tags. Public endpoint.
314    pub async fn fab_tag_groups(
315        &self,
316    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabTagGroup>, EpicAPIError> {
317        self.get_json("https://www.fab.com/i/tags/groups").await
318    }
319
320    /// Fetch available UE versions. Public endpoint.
321    pub async fn fab_ue_versions(
322        &self,
323    ) -> Result<Vec<String>, EpicAPIError> {
324        self.get_json("https://www.fab.com/i/unreal-engine/versions").await
325    }
326
327    /// Fetch channel info by slug. Public endpoint.
328    pub async fn fab_channel(
329        &self,
330        slug: &str,
331    ) -> Result<crate::api::types::fab_taxonomy::FabChannel, EpicAPIError> {
332        let url = format!("https://www.fab.com/i/channels/{}", slug);
333        self.get_json(&url).await
334    }
335
336    /// Search library entitlements with filters and aggregations.
337    /// Uses the browser-path Fab API. Requires Fab session cookies for full results.
338    pub async fn fab_library_entitlements(
339        &self,
340        params: &crate::api::types::fab_entitlement::FabEntitlementSearchParams,
341    ) -> Result<crate::api::types::fab_entitlement::FabEntitlementResults, EpicAPIError> {
342        let mut query_parts = Vec::new();
343        if let Some(ref sort_by) = params.sort_by {
344            query_parts.push(format!("sort_by={}", sort_by));
345        }
346        if let Some(ref cursor) = params.cursor {
347            query_parts.push(format!("cursor={}", cursor));
348        }
349        if let Some(ref listing_types) = params.listing_types {
350            query_parts.push(format!("listing_types={}", listing_types));
351        }
352        if let Some(ref categories) = params.categories {
353            query_parts.push(format!("categories={}", categories));
354        }
355        if let Some(ref tags) = params.tags {
356            query_parts.push(format!("tags={}", tags));
357        }
358        if let Some(ref licenses) = params.licenses {
359            query_parts.push(format!("licenses={}", licenses));
360        }
361        if let Some(ref asset_formats) = params.asset_formats {
362            query_parts.push(format!("asset_formats={}", asset_formats));
363        }
364        if let Some(ref source) = params.source {
365            query_parts.push(format!("source={}", source));
366        }
367        if let Some(ref aggregate_on) = params.aggregate_on {
368            query_parts.push(format!("aggregate_on={}", aggregate_on));
369        }
370        if let Some(count) = params.count {
371            query_parts.push(format!("count={}", count));
372        }
373        if let Some(ref added_since) = params.added_since {
374            query_parts.push(format!("added_since={}", added_since));
375        }
376
377        let url = if query_parts.is_empty() {
378            "https://www.fab.com/i/library/entitlements/search".to_string()
379        } else {
380            format!(
381                "https://www.fab.com/i/library/entitlements/search?{}",
382                query_parts.join("&")
383            )
384        };
385        self.authorized_get_json(&url).await
386    }
387
388    /// Initialize Fab CSRF token. Sets `fab_csrftoken` cookie on the client.
389    pub async fn fab_csrf(&self) -> Result<(), EpicAPIError> {
390        let parsed_url =
391            url::Url::parse("https://www.fab.com/i/csrf").map_err(|_| EpicAPIError::InvalidParams)?;
392        let response = self.client.get(parsed_url).send().await.map_err(|e| {
393            error!("{:?}", e);
394            EpicAPIError::NetworkError(e)
395        })?;
396        if response.status().is_success() {
397            Ok(())
398        } else {
399            let status = response.status();
400            let body = response.text().await.unwrap_or_default();
401            warn!("{} result: {}", status, body);
402            Err(EpicAPIError::HttpError { status, body })
403        }
404    }
405
406    /// Fetch Fab user context (country, currency, feature flags). Works with just CSRF token.
407    pub async fn fab_user_context(
408        &self,
409    ) -> Result<crate::api::types::fab_search::FabUserContext, EpicAPIError> {
410        self.get_json("https://www.fab.com/i/users/context").await
411    }
412}