use configfiles::{ConfigFiles, ConfigError}; use http::{Http, HttpError}; use models; use models::{Token, Game, Installer, Config}; use serde_json; use serde_json::Value; use std::collections::HashMap; use std::fs; use std::fs::File; use std::io; use std::io::Write; use std::path::Path; #[derive(Debug)] pub enum GogError { Error(&'static str), ConfigError(ConfigError), HttpError(HttpError), SerdeError(serde_json::Error), IOError(io::Error), } impl From for GogError { fn from(e: ConfigError) -> Self { GogError::ConfigError(e) } } impl From for GogError { fn from(e: HttpError) -> Self { GogError::HttpError(e) } } impl From for GogError { fn from(e: serde_json::Error) -> Self { GogError::SerdeError(e) } } impl From for GogError { fn from(e: io::Error) -> Self { GogError::IOError(e) } } pub struct Gog<'a> { client_id: String, client_secret: String, redirect_uri: String, games_uri: String, http_client: &'a mut Http, } impl<'a> Gog<'a> { pub fn new(http_client: &'a mut Http) -> Gog<'a> { Gog { client_id: String::from("46899977096215655"), client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"), redirect_uri: String::from("https://embed.gog.com/on_login_success?origin=client"), games_uri: String::from("https://embed.gog.com/user/data/games"), http_client: http_client, } } pub fn login(&mut self) -> Result<(), GogError> { let config = ConfigFiles::new(); let mut token: Token = match config.load("token.json") { Ok(value) => value, Err(_) => { let code = self.get_code()?; self.get_token(code.as_str())? } }; if token.is_expired() { token = self.refresh_token(token.refresh_token.as_str())?; } config.save("token.json", &token)?; let auth_header = format!("Authorization: Bearer {token}", token = token.access_token); self.http_client.add_header(auth_header.as_str())?; Ok(()) } pub fn sync(&mut self, storage_path: &str) -> Result<(), GogError> { let configfiles = ConfigFiles::new(); let mut config: Config = match configfiles.load("config.json") { Ok(value) => value, Err(_) => { error!("Configuration error, generating new one..."); Config { storage: String::from(storage_path), games: HashMap::new(), installers: HashMap::new(), extras: HashMap::new(), } } }; let game_ids = self.get_game_ids()?; for game_id in game_ids { let game_hash_saved = match config.games.get(&game_id.to_string()) { Some(value) => value.clone(), None => u64::min_value(), }; let game = self.get_game(game_id)?; let game_hash = models::get_hash(&game); if game_hash_saved == game_hash { info!("{} already up to date.", &game.title); continue; } let game_root = Path::new(storage_path).join(&game.title); fs::create_dir_all(&game_root)?; if !game.cd_key.is_empty() { let key_path = Path::new(game_root.as_os_str()).join("key.txt"); let mut key_file = File::create(&key_path)?; key_file.write_all(game.cd_key.as_bytes())? } for installer in game.installers { let installer_hash_saved = match config.installers.get(&installer.manual_url) { Some(value) => value.clone(), None => u64::min_value(), }; let installer_hash = models::get_hash(&installer); if installer_hash_saved == installer_hash { info!("{} already up to date.", &installer.manual_url); continue; } let installer_uri = format!("https://embed.gog.com{}", installer.manual_url); info!("downloading {} for {}...", &installer.manual_url, &game.title); self.http_client.download(installer_uri.as_str(), &game_root)?; config.installers.insert(installer.manual_url, installer_hash); configfiles.save("config.json", &config)?; } for extra in game.extras { let extra_hash_saved = match config.installers.get(&extra.manual_url) { Some(value) => value.clone(), None => u64::min_value(), }; let extra_hash = models::get_hash(&extra); if extra_hash_saved == extra_hash { info!("{} already up to date.", &extra.manual_url); continue; } let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); info!("downloading {} for {}...", &extra.name, &game.title); self.http_client.download(extra_uri.as_str(), &game_root)?; config.extras.insert(extra.manual_url, extra_hash); configfiles.save("config.json", &config)?; } config.games.insert(game_id.to_string(), game_hash); configfiles.save("config.json", &config)?; } Ok(()) } fn get_code(&self) -> Result { let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str()); println!("{}", auth_uri); let mut code = String::new(); print!("Code: "); io::stdout().flush()?; io::stdin() .read_line(&mut code) .expect("Failed to read line"); Ok(code) } fn get_token(&mut self, code: &str) -> Result { let token_uri = self.token_uri(self.client_id.as_str(), self.client_secret.as_str(), code, self.redirect_uri.as_str()); let token_response = match self.http_client.get(token_uri.as_str()) { Ok(value) => value, Err(error) => return Err(GogError::HttpError(error)), }; match serde_json::from_str(&token_response) { Ok(value) => Ok(value), Err(error) => Err(GogError::SerdeError(error)), } } fn refresh_token(&mut self, refresh_token: &str) -> Result { let token_refresh_uri = self.token_refresh_uri(self.client_id.as_str(), self.client_secret.as_str(), refresh_token); let token_response = match self.http_client.get(token_refresh_uri.as_str()) { Ok(value) => value, Err(error) => return Err(GogError::HttpError(error)), }; match serde_json::from_str(&token_response) { Ok(value) => Ok(value), Err(error) => Err(GogError::SerdeError(error)), } } fn get_game_ids(&mut self) -> Result, GogError> { let response = self.http_client.get(self.games_uri.as_str())?; let game_ids_raw: Value = serde_json::from_str(response.as_str())?; let game_ids_serde = &game_ids_raw["owned"]; let mut game_ids: Vec = Vec::new(); if !game_ids_serde.is_array() { return Err(GogError::Error("Error parsing game ids.")); } for game_id in game_ids_serde.as_array().unwrap() { let game_id_parsed = game_id.as_u64().unwrap_or(0); if game_id_parsed == 0 { error!("Cant parse game id {}", game_id); continue; } if [1, 2, 3, 4, 5].contains(&game_id_parsed) { continue; } game_ids.push(game_id_parsed); } return Ok(game_ids); } fn get_game(&mut self, game_id: u64) -> Result { let game_uri = self.game_uri(game_id); let response = self.http_client.get(game_uri.as_str())?; let mut game: Game = serde_json::from_str(&response)?; let game_raw: Value = serde_json::from_str(response.as_str())?; let downloads = &game_raw["downloads"]; for languages in downloads.as_array() { for language in languages { if !language.is_array() || language.as_array().unwrap().len() < 2 { error!("Skipping a language for {}", game.title); continue; } let installer_language = match language[0].as_str() { Some(value) => value, None => "", }; if installer_language == "" { error!("Skipping a language for {}", game.title); continue; } for systems in language[1].as_object() { for system in systems.keys() { for real_downloads in systems.get(system) { for real_download in real_downloads.as_array() { let download = &real_download[0]; if !download.is_object() || !download.as_object().unwrap().contains_key("manualUrl") { error!("Skipping an installer for {}", game.title); continue; } let installer = Installer { manual_url: String::from(download["manualUrl"] .as_str() .unwrap()), version: String::from(download["version"] .as_str() .unwrap_or("")), os: system.clone(), language: String::from(installer_language), }; game.installers.push(installer); } } } } } } Ok(game) } fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String { format!("https://auth.gog.\ com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2", client_id = client_id, redirect_uri = redirect_uri) } fn token_uri(&self, client_id: &str, client_secret: &str, code: &str, redirect_uri: &str) -> String { format!("https://auth.gog.\ com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}", client_id = client_id, client_secret = client_secret, code = code.trim(), redirect_uri = redirect_uri) } fn token_refresh_uri(&self, client_id: &str, client_secret: &str, refresh_token: &str) -> String { format!("https://auth.gog.\ com/token?client_id={client_id}&client_secret={client_secret}&grant_type=refresh_token&refresh_token={refresh_token}", client_id = client_id, client_secret = client_secret, refresh_token = refresh_token) } fn game_uri(&self, game_id: u64) -> String { format!("https://embed.gog.com/account/gameDetails/{game_id}.json", game_id = game_id) } }