#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
extern crate curl;
extern crate regex;
extern crate reqwest;
extern crate serde;
mod containers;
mod error;
pub mod extract;
pub mod gog;
pub mod token;
use connect::*;
use containers::*;
use curl::easy::Easy;
use curl::easy::{Easy2, Handler, WriteError};
use domains::*;
pub use error::Error;
pub use error::ErrorKind;
pub use error::Result;
use extract::EOCDOffset::*;
use extract::*;
use gog::*;
use product::*;
use regex::*;
use reqwest::header::*;
use reqwest::RedirectPolicy;
use reqwest::{Client, Method, Response};
use serde::de::DeserializeOwned;
use serde_json::value::{Map, Value};
use std::cell::RefCell;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use token::Token;
use ErrorKind::*;
const GET: Method = Method::GET;
const POST: Method = Method::POST;
pub type EmptyResponse = ::std::result::Result<Response, Error>;
macro_rules! map_p {
($($js: tt)+) => {
Some(json!($($js)+).as_object().unwrap().clone())
}
}
pub struct Gog {
pub token: RefCell<Token>,
pub client: RefCell<Client>,
pub client_noredirect: RefCell<Client>,
pub auto_update: bool,
}
impl Gog {
pub fn from_login_code(code: &str) -> Gog {
Gog::from_token(Token::from_login_code(code).unwrap())
}
pub fn new(token: Token) -> Gog {
Gog::from_token(token)
}
fn from_token(token: Token) -> Gog {
let headers = Gog::headers_token(&token.access_token);
let mut client = Client::builder();
let mut client_re = Client::builder().redirect(RedirectPolicy::none());
client = client.default_headers(headers.clone());
client_re = client_re.default_headers(headers);
return Gog {
token: RefCell::new(token),
client: RefCell::new(client.build().unwrap()),
client_noredirect: RefCell::new(client_re.build().unwrap()),
auto_update: true,
};
}
fn update_token(&self, token: Token) {
let headers = Gog::headers_token(&token.access_token);
let client = Client::builder();
let client_re = Client::builder().redirect(RedirectPolicy::none());
self.client
.replace(client.default_headers(headers.clone()).build().unwrap());
self.client_noredirect
.replace(client_re.default_headers(headers).build().unwrap());
self.token.replace(token);
}
pub fn uid_string(&self) -> String {
self.token.borrow().user_id.clone()
}
pub fn uid(&self) -> i64 {
self.token.borrow().user_id.parse().unwrap()
}
fn headers_token(at: &str) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"Authorization",
("Bearer ".to_string() + at).parse().unwrap(),
);
headers.insert("CSRF", "csrf=true".parse().unwrap());
return headers;
}
fn rget(
&self,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
) -> Result<Response> {
self.rreq(GET, domain, path, params)
}
fn rpost(
&self,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
) -> Result<Response> {
self.rreq(POST, domain, path, params)
}
fn rreq(
&self,
method: Method,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
) -> Result<Response> {
if self.token.borrow().is_expired() {
if self.auto_update {
self.update_token(self.token.borrow().refresh()?);
return self.rreq(method, domain, path, params);
} else {
return Err(ExpiredToken.into());
}
} else {
let mut url = domain.to_string() + path;
if params.is_some() {
let params = params.unwrap();
if params.len() > 0 {
url = url + "?";
for (k, v) in params.iter() {
url = url + k + "=" + &v.to_string() + "&";
}
url.pop();
}
}
Ok(self.client.borrow().request(method, &url).send()?)
}
}
fn fget<T>(&self, domain: &str, path: &str, params: Option<Map<String, Value>>) -> Result<T>
where
T: DeserializeOwned,
{
self.freq(GET, domain, path, params)
}
fn fpost<T>(&self, domain: &str, path: &str, params: Option<Map<String, Value>>) -> Result<T>
where
T: DeserializeOwned,
{
self.freq(POST, domain, path, params)
}
fn freq<T>(
&self,
method: Method,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
) -> Result<T>
where
T: DeserializeOwned,
{
let mut res = self.rreq(method, domain, path, params)?;
let st = res.text()?;
Ok(serde_json::from_str(&st)?)
}
fn nfreq<T>(
&self,
method: Method,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
nested: &str,
) -> Result<T>
where
T: DeserializeOwned,
{
let r: Map<String, Value> = self.freq(method, domain, path, params)?;
if r.contains_key(nested) {
return Ok(serde_json::from_str(&r.get(nested).unwrap().to_string())?);
} else {
return Err(MissingField(nested.to_string()).into());
}
}
fn nfget<T>(
&self,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
nested: &str,
) -> Result<T>
where
T: DeserializeOwned,
{
self.nfreq(GET, domain, path, params, nested)
}
fn nfpost<T>(
&self,
domain: &str,
path: &str,
params: Option<Map<String, Value>>,
nested: &str,
) -> Result<T>
where
T: DeserializeOwned,
{
self.nfreq(POST, domain, path, params, nested)
}
pub fn get_user_data(&self) -> Result<UserData> {
self.fget(EMBD, "/userData.json", None)
}
pub fn get_pub_info(&self, uid: i64, expand: Vec<String>) -> Result<PubInfo> {
self.fget(
EMBD,
&("/users/info/".to_string() + &uid.to_string()),
map_p!({
"expand":expand.iter().try_fold("".to_string(), fold_mult).unwrap()
}),
)
}
pub fn get_games(&self) -> Result<Vec<i64>> {
let r: OwnedGames = self.fget(EMBD, "/user/data/games", None)?;
Ok(r.owned)
}
pub fn get_game_details(&self, game_id: i64) -> Result<GameDetails> {
let mut res: GameDetailsP = self.fget(
EMBD,
&("/account/gameDetails/".to_string() + &game_id.to_string() + ".json"),
None,
)?;
if res.downloads.len() > 0 {
res.downloads[0].remove(0);
let downloads: Downloads =
serde_json::from_str(&serde_json::to_string(&res.downloads[0][0])?)?;
Ok(res.to_details(downloads))
} else {
Err(NotAvailable.into())
}
}
pub fn download_game(&self, downloads: Vec<Download>) -> Vec<Result<Response>> {
downloads
.iter()
.map(|x| {
let mut url = BASE.to_string() + &x.manual_url;
let mut response;
loop {
let temp_response = self.client_noredirect.borrow().get(&url).send();
if temp_response.is_ok() {
response = temp_response.unwrap();
let headers = response.headers();
if headers.contains_key("location") {
url = headers
.get("location")
.unwrap()
.to_str()
.unwrap()
.to_string();
} else {
break;
}
} else {
return Err(temp_response.err().unwrap().into());
}
}
Ok(response)
})
.collect()
}
pub fn hide_product(&self, game_id: i64) -> EmptyResponse {
self.rget(
EMBD,
&("/account/hideProduct".to_string() + &game_id.to_string()),
None,
)
}
pub fn reveal_product(&self, game_id: i64) -> EmptyResponse {
self.rget(
EMBD,
&("/account/revealProduct".to_string() + &game_id.to_string()),
None,
)
}
pub fn wishlist(&self) -> Result<Wishlist> {
self.fget(EMBD, "/user/wishlist.json", None)
}
pub fn add_wishlist(&self, game_id: i64) -> Result<Wishlist> {
self.fget(
EMBD,
&("/user/wishlist/add/".to_string() + &game_id.to_string()),
None,
)
}
pub fn rm_wishlist(&self, game_id: i64) -> Result<Wishlist> {
self.fget(
EMBD,
&("/user/wishlist/remove/".to_string() + &game_id.to_string()),
None,
)
}
pub fn save_birthday(&self, bday: &str) -> EmptyResponse {
self.rget(EMBD, &("/account/save_birthday".to_string() + bday), None)
}
pub fn save_country(&self, country: &str) -> EmptyResponse {
self.rget(EMBD, &("/account/save_country".to_string() + country), None)
}
pub fn save_currency(&self, currency: Currency) -> EmptyResponse {
self.rget(
EMBD,
&("/user/changeCurrency".to_string() + ¤cy.to_string()),
None,
)
}
pub fn save_language(&self, language: Language) -> EmptyResponse {
self.rget(
EMBD,
&("/user/changeLanguage".to_string() + &language.to_string()),
None,
)
}
pub fn connect_account(&self, user_id: i64) -> Result<LinkedSteam> {
self.fget(
EMBD,
&("/api/v1/users/".to_string() + &user_id.to_string() + "/gogLink/steam/linkedAccount"),
None,
)
}
pub fn connect_status(&self, user_id: i64) -> Result<ConnectStatus> {
let st = self
.rget(
EMBD,
&("/api/v1/users/".to_string()
+ &user_id.to_string()
+ "/gogLink/steam/exchangeableProducts"),
None,
)?
.text()?;
if let Ok(cs) = serde_json::from_str(&st) {
return Ok(cs);
} else {
let map: Map<String, Value> = serde_json::from_str(&st)?;
if let Some(items) = map.get("items") {
let array = items.as_array();
if array.is_some() && array.unwrap().len() == 0 {
return Err(NotAvailable.into());
}
}
}
Err(MissingField("items".to_string()).into())
}
pub fn connect_scan(&self, user_id: i64) -> EmptyResponse {
self.rget(
EMBD,
&("/api/v1/users/".to_string()
+ &user_id.to_string()
+ "/gogLink/steam/synchronizeUserProfile"),
None,
)
}
pub fn connect_claim(&self, user_id: i64) -> EmptyResponse {
self.rget(
EMBD,
&("/api/v1/users/".to_string() + &user_id.to_string() + "/gogLink/steam/claimProducts"),
None,
)
}
pub fn product(&self, ids: Vec<i64>, expand: Vec<String>) -> Result<Vec<Product>> {
self.fget(
API,
"/products",
map_p!({
"expand":expand.iter().try_fold("".to_string(), fold_mult).unwrap(),
"ids": ids.iter().try_fold("".to_string(), |acc, x|{
let done : Result<String> = Ok(acc +"," +&x.to_string());
done
}).unwrap()
}),
)
}
pub fn achievements(&self, product_id: i64, user_id: i64) -> Result<AchievementList> {
self.fget(
GPLAY,
&("/clients/".to_string()
+ &product_id.to_string()
+ "/users/"
+ &user_id.to_string()
+ "/achievements"),
None,
)
}
pub fn add_tag(&self, product_id: i64, tag_id: i64) -> Result<bool> {
let res: Result<Success> = self.fget(
EMBD,
"/account/tags/attach",
map_p!({
"product_id":product_id,
"tag_id":tag_id
}),
);
res.map(|x| x.success)
}
pub fn rm_tag(&self, product_id: i64, tag_id: i64) -> Result<bool> {
self.nfget(
EMBD,
"/account/tags/detach",
map_p!({
"product_id":product_id,
"tag_id":tag_id
}),
"success",
)
}
pub fn get_filtered_products(&self, params: FilterParams) -> Result<FilteredProducts> {
let url = reqwest::Url::parse(
&("https://gog.com/account/getFilteredProducts".to_string()
+ ¶ms.to_query_string()),
)
.unwrap();
let path = url.path().to_string() + "?" + url.query().unwrap();
self.fget(EMBD, &path, None)
}
pub fn get_all_filtered_products(&self, params: FilterParams) -> Result<Vec<ProductDetails>> {
let url = reqwest::Url::parse(
&("https://gog.com/account/getFilteredProducts".to_string()
+ ¶ms.to_query_string()),
)
.unwrap();
let mut page = 1;
let path = url.path().to_string() + "?" + url.query().unwrap();
let mut products = vec![];
loop {
let res: FilteredProducts =
self.fget(EMBD, &format!("{}&page={}", path, page), None)?;
products.push(res.products);
if res.page <= page {
break;
} else {
page += 1;
}
}
Ok(products.into_iter().flatten().collect())
}
pub fn get_products(&self, params: FilterParams) -> Result<Vec<UnownedProductDetails>> {
let url = reqwest::Url::parse(
&("https://gog.com/games/ajax/filtered".to_string() + ¶ms.to_query_string()),
)
.unwrap();
let path = url.path().to_string() + "?" + url.query().unwrap();
self.nfget(EMBD, &path, None, "products")
}
pub fn create_tag(&self, name: &str) -> Result<i64> {
return self
.nfget(EMBD, "/account/tags/add", map_p!({ "name": name }), "id")
.map(|x: String| x.parse::<i64>().unwrap());
}
pub fn delete_tag(&self, tag_id: i64) -> Result<bool> {
let res: Result<StatusDel> =
self.fget(EMBD, "/account/tags/delete", map_p!({ "tag_id": tag_id }));
res.map(|x| {
if x.status.as_str() == "deleted" {
return true;
} else {
return false;
}
})
}
pub fn newsletter_subscription(&self, enabled: bool) -> EmptyResponse {
self.rget(
EMBD,
&("/account/save_newsletter_subscription/".to_string()
+ &bool_to_int(enabled).to_string()),
None,
)
}
pub fn promo_subscription(&self, enabled: bool) -> EmptyResponse {
self.rget(
EMBD,
&("/account/save_promo_subscription/".to_string() + &bool_to_int(enabled).to_string()),
None,
)
}
pub fn wishlist_subscription(&self, enabled: bool) -> EmptyResponse {
self.rget(
EMBD,
&("/account/save_wishlist_notification/".to_string()
+ &bool_to_int(enabled).to_string()),
None,
)
}
pub fn all_subscription(&self, enabled: bool) -> Vec<EmptyResponse> {
vec![
self.newsletter_subscription(enabled),
self.promo_subscription(enabled),
self.wishlist_subscription(enabled),
]
}
pub fn game_ratings(&self) -> Result<Vec<(String, i64)>> {
let g: Map<String, Value> =
self.nfget(EMBD, "/user/games_rating.json", None, "games_rating")?;
Ok(g.iter()
.map(|x| return (x.0.to_owned(), x.1.as_i64().unwrap()))
.collect::<Vec<(String, i64)>>())
}
pub fn voted_reviews(&self) -> Result<Vec<i64>> {
return self.nfget(EMBD, "/user/review_votes.json", None, "reviews");
}
pub fn report_review(&self, review_id: i32) -> Result<bool> {
self.nfpost(
EMBD,
&("/reviews/report/review/".to_string() + &review_id.to_string() + ".json"),
None,
"reported",
)
}
pub fn library_background(&self, bg: ShelfBackground) -> EmptyResponse {
self.rpost(
EMBD,
&("/account/save_shelf_background/".to_string() + bg.as_str()),
None,
)
}
pub fn friends(&self) -> Result<Vec<Friend>> {
self.nfget(
CHAT,
&("/users/".to_string() + &self.uid_string() + "/friends"),
None,
"items",
)
}
fn get_sizes<R: Read>(&self, bufreader: &mut BufReader<R>) -> Result<(usize, usize)> {
let mut buffer = String::new();
let mut script_size = 0;
let mut script_bytes = 0;
let mut script = String::new();
let mut i = 1;
let mut filesize = 0;
let filesize_reg = Regex::new(r#"filesizes="(\d+)"#).unwrap();
let offset_reg = Regex::new(r"offset=`head -n (\d+)").unwrap();
loop {
let read = bufreader.read_line(&mut buffer).unwrap();
script_bytes += read;
if script_size != 0 && script_size > i {
script += &buffer;
} else if script_size != 0 && script_size <= i && filesize != 0 {
break;
}
if script_size == 0 {
let captures = offset_reg.captures(&buffer);
if captures.is_some() {
let un = captures.unwrap();
if un.len() > 1 {
script_size = un[1].to_string().parse().unwrap();
}
}
}
if filesize == 0 {
let captures = filesize_reg.captures(&buffer);
if captures.is_some() {
let un = captures.unwrap();
if un.len() > 1 {
filesize = un[1].to_string().parse().unwrap();
}
}
}
i += 1;
}
Ok((script_bytes, filesize))
}
pub fn download_request_range_at<H: Handler>(
at: impl Into<String>,
url: impl Into<String>,
handler: H,
start: i64,
end: i64,
) -> Result<Easy2<H>> {
let mut url = url.into();
let mut easy = Easy2::new(handler);
easy.url(&url)?;
easy.range(&format!("{}-{}", start, end))?;
easy.follow_location(true)?;
let mut list = curl::easy::List::new();
list.append("CSRF: true")?;
list.append(&format!("Authentication: Bearer {}", at.into()))?;
easy.get(true)?;
easy.http_headers(list)?;
easy.perform()?;
Ok(easy)
}
pub fn download_request_range(
&self,
url: impl Into<String>,
start: i64,
end: i64,
) -> Result<Vec<u8>> {
Ok(Gog::download_request_range_at(
self.token.borrow().access_token.as_str(),
url,
Collector(Vec::new()),
start,
end,
)?
.get_ref()
.0
.clone())
}
pub fn extract_data(&self, downloads: Vec<Download>) -> Result<Vec<ZipData>> {
let mut zips = vec![];
let mut responses = self.download_game(downloads.clone());
for down in downloads {
let mut url = BASE.to_string() + &down.manual_url;
let mut response;
loop {
let temp_response = self.client_noredirect.borrow().get(&url).send();
if temp_response.is_ok() {
response = temp_response.unwrap();
let headers = response.headers();
if headers.contains_key("location") {
url = headers
.get("location")
.unwrap()
.to_str()
.unwrap()
.to_string();
} else {
break;
}
}
}
let response = responses.remove(0).expect("Couldn't get download");
let size = response
.headers()
.get(CONTENT_LENGTH)
.unwrap()
.to_str()
.expect("Couldn't convert to string")
.parse()
.unwrap();
let mut bufreader = BufReader::new(response);
let sizes = self.get_sizes(&mut bufreader)?;
let offset = sizes.0 + sizes.1;
let eocd_offset = self.get_eocd_offset(&url, size)?;
let mut off = 0;
match eocd_offset {
EOCDOffset::Offset(offset) => {
off = offset;
}
EOCDOffset::Offset64(offset) => {
off = offset;
}
};
let mut cd_offset = 0;
let mut records = 0;
let mut cd_size = 0;
let mut central_directory =
self.download_request_range(url.as_str(), off as i64, size)?;
let mut cd_slice = central_directory.as_slice();
let mut cd_reader = BufReader::new(&mut cd_slice);
match eocd_offset {
EOCDOffset::Offset(..) => {
let cd = CentralDirectory::from_reader(&mut cd_reader);
cd_offset = cd.cd_start_offset as u64;
records = cd.total_cd_records as u64;
cd_size = cd.cd_size as u64;
}
EOCDOffset::Offset64(..) => {
let cd = CentralDirectory64::from_reader(&mut cd_reader);
cd_offset = cd.cd_start as u64;
records = cd.cd_total;
cd_size = cd.cd_size as u64;
}
};
let mut offset_beg = sizes.0 + sizes.1 + cd_offset as usize;
let mut cd = self
.download_request_range(
url.as_str(),
offset_beg as i64,
(offset_beg + cd_size as usize) as i64,
)
.unwrap();
let mut slice = cd.as_slice();
let mut full_reader = BufReader::new(&mut slice);
let mut files = vec![];
for i in 0..records {
let mut entry = CDEntry::from_reader(&mut full_reader);
entry.start_offset = (sizes.0 + sizes.1) as u64 + entry.disk_offset.unwrap();
files.push(entry);
}
let len = files.len();
files[len - 1].end_offset = offset_beg as u64 - 1;
for i in 0..(len - 1) {
files[i].end_offset = files[i + 1].start_offset;
}
zips.push(ZipData {
sizes: sizes,
files: files,
url: url.clone(),
cd: None,
});
}
Ok(zips)
}
fn get_eocd_offset(&self, url: &str, size: i64) -> Result<EOCDOffset> {
let signature = 0x06054b50;
let signature_64 = 0x06064b50;
let mut offset = 0;
let mut inter = [0; 4];
let mut easy = Easy::new();
for i in 4..size + 1 {
let pos = size - i;
let resp = self.download_request_range(url, pos, pos + 4)?;
let cur = pos + 4;
let inte = vec_to_u32(&resp);
if inte == signature {
offset = cur;
offset -= 4;
return Ok(EOCDOffset::Offset(offset as usize));
} else if inte == signature_64 {
offset = cur;
offset -= 4;
return Ok(EOCDOffset::Offset64(offset as usize));
}
}
Err(NotAvailable.into())
}
}
fn fold_mult(acc: String, now: &String) -> Result<String> {
return Ok(acc + "," + now);
}
fn bool_to_int(b: bool) -> i32 {
let mut par = 0;
if b {
par = 1;
}
return par;
}
fn vec_to_u32(data: &Vec<u8>) -> u32 {
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
}
pub struct Collector(pub Vec<u8>);
impl Handler for Collector {
fn write(&mut self, data: &[u8]) -> std::result::Result<usize, WriteError> {
self.0.extend_from_slice(data);
Ok(data.len())
}
}