Skip to main content

egs_api/
lib.rs

1#![deny(missing_docs)]
2#![cfg_attr(test, deny(warnings))]
3
4//! # Epic Games Store API
5//!
6//! Async Rust client for the Epic Games Store API. Provides authentication,
7//! asset management, download manifest parsing (binary + JSON), and
8//! [Fab](https://www.fab.com/) marketplace integration.
9//!
10//! # Quick Start
11//!
12//! ```rust,no_run
13//! use egs_api::EpicGames;
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let mut egs = EpicGames::new();
18//!
19//!     // Authenticate with an authorization code
20//!     let code = "your_authorization_code".to_string();
21//!     if egs.auth_code(None, Some(code)).await {
22//!         println!("Logged in as {}", egs.user_details().display_name.unwrap_or_default());
23//!     }
24//!
25//!     // List all owned assets
26//!     let assets = egs.list_assets(None, None).await;
27//!     println!("You own {} assets", assets.len());
28//! }
29//! ```
30//!
31//! # Authentication
32//!
33//! Epic uses OAuth2 with a public launcher client ID. The flow is:
34//!
35//! 1. Open the [authorization URL] in a browser — the user logs in and gets
36//!    redirected to a JSON page with an `authorizationCode`.
37//! 2. Pass that code to [`EpicGames::auth_code`].
38//! 3. Persist the session with [`EpicGames::user_details`] (implements
39//!    `Serialize` / `Deserialize`).
40//! 4. Restore it later with [`EpicGames::set_user_details`] +
41//!    [`EpicGames::login`], which uses the refresh token.
42//!
43//! [authorization URL]: https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect%3FclientId%3D34a02cf8f4414e29b15921876da36f9a%26responseType%3Dcode
44//!
45//! # Features
46//!
47//! - **Assets** — List owned assets, fetch catalog metadata (with DLC trees),
48//!   retrieve asset manifests with CDN download URLs.
49//! - **Download Manifests** — Parse Epic's binary and JSON manifest formats.
50//!   Exposes file lists, chunk hashes, and custom fields for download
51//!   reconstruction.
52//! - **Fab Marketplace** — List Fab library items, fetch signed asset manifests,
53//!   and download manifests from distribution points.
54//! - **Account** — Details, bulk ID lookup, friends list.
55//! - **Entitlements** — Games, DLC, subscriptions.
56//! - **Library** — Paginated listing with optional metadata.
57//! - **Tokens** — Game exchange tokens and per-asset ownership tokens (JWT).
58//!
59//! # Architecture
60//!
61//! [`EpicGames`] is the public facade. It wraps an internal `EpicAPI` struct
62//! that holds the `reqwest::Client` (with cookie store) and session state.
63//! Most public methods return `Option<T>` or `Vec<T>`, swallowing transport
64//! errors for convenience. Fab methods return `Result<T, EpicAPIError>` to
65//! expose timeout/error distinctions.
66//!
67//! # Examples
68//!
69//! The crate ships with examples covering every endpoint. See the
70//! [`examples/`](https://github.com/AchetaGames/egs-api-rs/tree/master/examples)
71//! directory or run:
72//!
73//! ```bash
74//! cargo run --example auth                # Interactive login + token persistence
75//! cargo run --example account             # Account details, ID lookup, friends, external auths, SSO
76//! cargo run --example entitlements        # List all entitlements
77//! cargo run --example library             # Paginated library listing
78//! cargo run --example assets              # Full pipeline: list → info → manifest → download
79//! cargo run --example game_token          # Exchange code + ownership token
80//! cargo run --example fab                 # Fab library → asset manifest → download manifest
81//! cargo run --example catalog             # Catalog items, offers, bulk lookup
82//! cargo run --example commerce            # Currencies, prices, billing, quick purchase
83//! cargo run --example status              # Service status (lightswitch API)
84//! cargo run --example presence            # Update online presence
85//! cargo run --example client_credentials  # App-level auth + library state tokens
86//! ```
87
88use crate::api::EpicAPI;
89
90/// Module for authenticated API communication
91pub mod api;
92
93mod facade;
94
95/// Client for the Epic Games Store API.
96///
97/// This is the main entry point for the library. Create an instance with
98/// [`EpicGames::new`], authenticate with [`EpicGames::auth_code`] or
99/// [`EpicGames::login`], then call API methods.
100///
101/// Most methods return `Option<T>` or `Vec<T>`, returning `None` / empty on
102/// errors. Fab methods return `Result<T, EpicAPIError>` for richer error
103/// handling (e.g., distinguishing timeouts from other failures).
104///
105/// Session state is stored in [`UserData`](crate::api::types::account::UserData),
106/// which implements `Serialize` / `Deserialize` for persistence across runs.
107#[derive(Debug, Clone)]
108pub struct EpicGames {
109    egs: EpicAPI,
110}
111
112impl Default for EpicGames {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl EpicGames {
119    /// Creates a new [`EpicGames`] client.
120    pub fn new() -> Self {
121        EpicGames {
122            egs: EpicAPI::new(),
123        }
124    }
125}
126
127#[cfg(test)]
128mod facade_tests {
129    use super::*;
130    use crate::api::types::account::UserData;
131    use chrono::{Duration, Utc};
132
133    #[test]
134    fn new_creates_instance() {
135        let egs = EpicGames::new();
136        assert!(!egs.is_logged_in());
137    }
138
139    #[test]
140    fn default_same_as_new() {
141        let egs = EpicGames::default();
142        assert!(!egs.is_logged_in());
143    }
144
145    #[test]
146    fn user_details_default_empty() {
147        let egs = EpicGames::new();
148        assert!(egs.user_details().access_token.is_none());
149    }
150
151    #[test]
152    fn set_and_get_user_details() {
153        let mut egs = EpicGames::new();
154        let mut ud = UserData::new();
155        ud.display_name = Some("TestUser".to_string());
156        egs.set_user_details(ud);
157        assert_eq!(
158            egs.user_details().display_name,
159            Some("TestUser".to_string())
160        );
161    }
162
163    #[test]
164    fn is_logged_in_expired() {
165        let mut egs = EpicGames::new();
166        let mut ud = UserData::new();
167        ud.expires_at = Some(Utc::now() - Duration::hours(1));
168        egs.set_user_details(ud);
169        assert!(!egs.is_logged_in());
170    }
171
172    #[test]
173    fn is_logged_in_valid() {
174        let mut egs = EpicGames::new();
175        let mut ud = UserData::new();
176        ud.expires_at = Some(Utc::now() + Duration::hours(2));
177        egs.set_user_details(ud);
178        assert!(egs.is_logged_in());
179    }
180
181    #[test]
182    fn is_logged_in_within_600s_threshold() {
183        let mut egs = EpicGames::new();
184        let mut ud = UserData::new();
185        ud.expires_at = Some(Utc::now() + Duration::seconds(500));
186        egs.set_user_details(ud);
187        assert!(!egs.is_logged_in());
188    }
189}