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}