Move GOG website related members of Downloader class to new Website class

Downloader::HTTP_Login --> Website::Login
Downloader::getGames --> Website::getGames
Downloader::getFreeGames --> Website::getFreeGames
Downloader::getGameDetailsJSON --> Website::getGameDetailsJSON
Getting wishlist details is split from Downloader::showWishlist to Website::getWishlistItems function
This commit is contained in:
Sude 2016-04-18 20:33:19 +03:00
parent 519cb78d38
commit 33fb004cdd
6 changed files with 811 additions and 620 deletions

View File

@ -12,7 +12,7 @@ find_package(Boost
program_options
date_time
)
find_package(CURL REQUIRED)
find_package(CURL 7.17.1 REQUIRED)
find_package(OAuth REQUIRED)
find_package(Jsoncpp REQUIRED)
find_package(Htmlcxx REQUIRED)
@ -22,6 +22,7 @@ find_package(Rhash REQUIRED)
file(GLOB SRC_FILES
main.cpp
src/api.cpp
src/website.cpp
src/downloader.cpp
src/progressbar.cpp
src/util.cpp

View File

@ -24,6 +24,7 @@
#include "config.h"
#include "api.h"
#include "progressbar.h"
#include "website.h"
#include <curl/curl.h>
#include <json/json.h>
#include <ctime>
@ -47,14 +48,6 @@ class Timer
struct timeval last_update;
};
class gameItem {
public:
std::string name;
std::string id;
std::vector<std::string> dlcnames;
Json::Value gamedetailsjson;
};
class Downloader
{
public:
@ -91,11 +84,7 @@ class Downloader
int loadGameDetailsCache();
int saveGameDetailsCache();
std::vector<gameDetails> getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0);
int HTTP_Login(const std::string& email, const std::string& password);
std::vector<gameItem> getGames();
std::vector<gameItem> getFreeGames();
std::vector<gameFile> getExtrasFromJSON(const Json::Value& json, const std::string& gamename);
Json::Value getGameDetailsJSON(const std::string& gameid);
std::string getSerialsFromJSON(const Json::Value& json);
void saveSerials(const std::string& serials, const std::string& filepath);
std::string getChangelogFromJSON(const Json::Value& json);
@ -106,7 +95,7 @@ class Downloader
static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream);
static size_t readData(void *ptr, size_t size, size_t nmemb, FILE *stream);
Website *gogWebsite;
API *gogAPI;
std::vector<gameItem> gameItems;
std::vector<gameDetails> games;

View File

@ -43,6 +43,30 @@ struct gameSpecificConfig
std::vector<unsigned int> vPlatformPriority;
};
struct gameItem
{
std::string name;
std::string id;
std::vector<std::string> dlcnames;
Json::Value gamedetailsjson;
};
struct wishlistItem
{
std::string title;
unsigned int platform;
std::vector<std::string> tags;
time_t release_date_time;
std::string currency;
std::string price;
std::string discount_percent;
std::string discount;
std::string store_credit;
std::string url;
bool bIsBonusStoreCreditIncluded;
bool bIsDiscounted;
};
namespace Util
{
std::string makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory = "", const unsigned int& platformId = 0, const std::string& dlcname = "");

38
include/website.h Normal file
View File

@ -0,0 +1,38 @@
/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://www.wtfpl.net/ for more details. */
#ifndef WEBSITE_H
#define WEBSITE_H
#include "config.h"
#include "util.h"
#include <curl/curl.h>
#include <json/json.h>
#include <fstream>
class Website
{
public:
Website(Config &conf);
int Login(const std::string& email, const std::string& password);
std::string getResponse(const std::string& url);
Json::Value getGameDetailsJSON(const std::string& gameid);
std::vector<gameItem> getGames();
std::vector<gameItem> getFreeGames();
std::vector<wishlistItem> getWishlistItems();
bool IsLoggedIn();
virtual ~Website();
protected:
private:
static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
CURL* curlhandle;
Config config;
bool IsloggedInSimple();
bool IsLoggedInComplex(const std::string& email);
int retries;
};
#endif // WEBSITE_H

View File

@ -23,7 +23,6 @@
#include <json/json.h>
#include <htmlcxx/html/ParserDom.h>
#include <htmlcxx/html/Uri.h>
#include <boost/algorithm/string/case_conv.hpp>
namespace bptime = boost::posix_time;
@ -42,6 +41,7 @@ Downloader::~Downloader()
this->report_ofs.close();
delete progressbar;
delete gogAPI;
delete gogWebsite;
curl_easy_cleanup(curlhandle);
curl_global_cleanup();
// Make sure that cookie file is only readable/writable by owner
@ -67,8 +67,6 @@ int Downloader::init()
curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout);
curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this);
curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str());
curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str());
curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose);
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
@ -80,6 +78,10 @@ int Downloader::init()
curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30);
curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
// Create new GOG website handle
gogWebsite = new Website(config);
bool bWebsiteIsLoggedIn = gogWebsite->IsLoggedIn();
// Create new API handle and set curl options for the API
gogAPI = new API(config.sToken, config.sSecret);
gogAPI->curlSetOpt(CURLOPT_VERBOSE, config.bVerbose);
@ -89,7 +91,7 @@ int Downloader::init()
progressbar = new ProgressBar(config.bUnicode, config.bColor);
bool bInitOK = gogAPI->init(); // Initialize the API
if (!bInitOK || config.bLoginHTTP || config.bLoginAPI)
if (!bInitOK || !bWebsiteIsLoggedIn || config.bLoginHTTP || config.bLoginAPI)
return 1;
if (config.bCover && config.bDownload && !config.bUpdateCheck)
@ -139,7 +141,7 @@ int Downloader::login()
// Login to website
if (config.bLoginHTTP)
{
if (!HTTP_Login(email, password))
if (!gogWebsite->Login(email, password))
{
std::cerr << "HTTP: Login failed" << std::endl;
return 0;
@ -197,11 +199,11 @@ void Downloader::getGameList()
{
if (config.sGameRegex == "free")
{
gameItems = this->getFreeGames();
gameItems = gogWebsite->getFreeGames();
}
else
{
gameItems = this->getGames();
gameItems = gogWebsite->getGames();
}
}
@ -321,19 +323,19 @@ int Downloader::getGameDetails()
if (game.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
game.extras = this->getExtrasFromJSON(gameDetailsJSON, gameItems[i].name);
}
if (config.bSaveSerials)
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
game.serials = this->getSerialsFromJSON(gameDetailsJSON);
}
if (config.bSaveChangelogs)
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
game.changelog = this->getChangelogFromJSON(gameDetailsJSON);
}
@ -341,7 +343,7 @@ int Downloader::getGameDetails()
if (game.dlcs.empty() && !bHasDLC && conf.bDLC && conf.bIgnoreDLCCount)
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
gameItems[i].dlcnames = Util::getDLCNamesFromJSON(gameDetailsJSON["dlcs"]);
bHasDLC = !gameItems[i].dlcnames.empty();
@ -357,7 +359,7 @@ int Downloader::getGameDetails()
if (dlc.extras.empty() && config.bExtras) // Try to get extras from account page if API didn't return any extras
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
// Make sure we get extras for the right DLC
for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
@ -379,7 +381,7 @@ int Downloader::getGameDetails()
if (config.bSaveSerials)
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
// Make sure we save serial for the right DLC
for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
@ -404,7 +406,7 @@ int Downloader::getGameDetails()
if (config.bSaveChangelogs)
{
if (gameDetailsJSON.empty())
gameDetailsJSON = this->getGameDetailsJSON(gameItems[i].id);
gameDetailsJSON = gogWebsite->getGameDetailsJSON(gameItems[i].id);
// Make sure we save changelog for the right DLC
for (unsigned int k = 0; k < gameDetailsJSON["dlcs"].size(); ++k)
@ -2000,468 +2002,6 @@ uintmax_t Downloader::getResumePosition()
return this->resume_position;
}
// Login to GOG website
int Downloader::HTTP_Login(const std::string& email, const std::string& password)
{
int res = 0;
std::string postdata;
std::ostringstream memory;
std::string token;
std::string tagname_username;
std::string tagname_password;
std::string tagname_login;
std::string tagname_token;
// Get login token
std::string html = this->getResponse("https://www.gog.com/");
htmlcxx::HTML::ParserDom parser;
tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
tree<htmlcxx::HTML::Node>::iterator end = dom.end();
// Find auth_url
bool bFoundAuthUrl = false;
for (; it != end; ++it)
{
if (it->tagName()=="script")
{
std::string auth_url;
for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
{
tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i);
if (!script_it->isTag() && !script_it->isComment())
{
if (script_it->text().find("GalaxyAccounts") != std::string::npos)
{
boost::match_results<std::string::const_iterator> what;
boost::regex expression(".*'(https://auth.gog.com/.*?)'.*");
boost::regex_match(script_it->text(), what, expression);
auth_url = what[1];
break;
}
}
}
if (!auth_url.empty())
{ // Found auth_url, get the necessary info for login
bFoundAuthUrl = true;
std::string login_form_html = this->getResponse(auth_url);
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::HTTP_Login)" << std::endl;
std::cerr << login_form_html << std::endl;
#endif
if (login_form_html.find("google.com/recaptcha") != std::string::npos)
{
std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl
<< "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl;
return res = 0;
}
tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end();
for (; login_it != login_it_end; ++login_it)
{
if (login_it->tagName()=="input")
{
login_it->parseAttributes();
std::string id_login = login_it->attribute("id").second;
if (id_login == "login_username")
{
tagname_username = login_it->attribute("name").second;
}
else if (id_login == "login_password")
{
tagname_password = login_it->attribute("name").second;
}
else if (id_login == "login__token")
{
token = login_it->attribute("value").second; // login token
tagname_token = login_it->attribute("name").second;
}
}
else if (login_it->tagName()=="button")
{
login_it->parseAttributes();
std::string id_login = login_it->attribute("id").second;
if (id_login == "login_login")
{
tagname_login = login_it->attribute("name").second;
}
}
}
break;
}
}
}
if (!bFoundAuthUrl)
{
std::cout << "Failed to find url for login form" << std::endl;
}
if (token.empty())
{
std::cout << "Failed to get login token" << std::endl;
return res = 0;
}
//Create postdata - escape characters in email/password to support special characters
postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size())
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size())
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "="
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size());
curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check");
curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
// Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
CURLcode result = curl_easy_perform(curlhandle);
memory.str(std::string());
if (result != CURLE_OK)
{
// Expected to hit maximum amount of redirects so don't print error on it
if (result != CURLE_TOO_MANY_REDIRECTS)
std::cout << curl_easy_strerror(result) << std::endl;
}
// Get redirect url
char *redirect_url;
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
// Handle two step authorization
if (std::string(redirect_url).find("two_step") != std::string::npos)
{
std::string security_code, tagname_two_step_send, tagname_two_step_auth_letter_1, tagname_two_step_auth_letter_2, tagname_two_step_auth_letter_3, tagname_two_step_auth_letter_4, tagname_two_step_token, token_two_step;
std::string two_step_html = this->getResponse(redirect_url);
redirect_url = NULL;
tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end();
for (; two_step_it != two_step_it_end; ++two_step_it)
{
if (two_step_it->tagName()=="input")
{
two_step_it->parseAttributes();
std::string id_two_step = two_step_it->attribute("id").second;
if (id_two_step == "second_step_authentication_token_letter_1")
{
tagname_two_step_auth_letter_1 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_2")
{
tagname_two_step_auth_letter_2 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_3")
{
tagname_two_step_auth_letter_3 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_4")
{
tagname_two_step_auth_letter_4 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication__token")
{
token_two_step = two_step_it->attribute("value").second; // two step token
tagname_two_step_token = two_step_it->attribute("name").second;
}
}
else if (two_step_it->tagName()=="button")
{
two_step_it->parseAttributes();
std::string id_two_step = two_step_it->attribute("id").second;
if (id_two_step == "second_step_authentication_send")
{
tagname_two_step_send = two_step_it->attribute("name").second;
}
}
}
std::cerr << "Security code: ";
std::getline(std::cin,security_code);
if (security_code.size() != 4)
{
std::cerr << "Security code must be 4 characters long" << std::endl;
exit(1);
}
postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "="
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size());
curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step");
curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
// Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
result = curl_easy_perform(curlhandle);
memory.str(std::string());
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
}
curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1);
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
result = curl_easy_perform(curlhandle);
if (result != CURLE_OK)
{
std::cout << curl_easy_strerror(result) << std::endl;
}
html = this->getResponse("https://www.gog.com/account/settings/personal");
std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it
dom = parser.parseTree(html);
it = dom.begin();
end = dom.end();
for (; it != end; ++it)
{
if (it->tagName()=="strong")
{
it->parseAttributes();
if (it->attribute("class").second == "settings-item__value settings-item__section")
{
for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
{
tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i);
if (!tag_it->isTag() && !tag_it->isComment())
{
std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text());
if (tag_text == email_lowercase)
{
res = 1; // Login successful
break;
}
}
}
}
}
if (res == 1) // Login was successful so no need to go through the remaining tags
break;
}
// Simple login check if complex check failed. Check login by trying to get account page. If response code isn't 200 then login failed.
if (res == 0)
{
std::string url = "https://www.gog.com/account";
curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_perform(curlhandle);
memory.str(std::string());
long int response_code = 0;
curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
if (response_code == 200)
res = 1; // Login successful
}
return res;
}
// Get list of games from account page
std::vector<gameItem> Downloader::getGames()
{
std::vector<gameItem> games;
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
int i = 1;
bool bAllPagesParsed = false;
do
{
std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
// Parse JSON
if (!jsonparser->parse(response, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << response << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
if (!response.empty())
{
if(response[0] != '{')
{
// Response was not JSON. Assume that cookies have expired.
std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl;
}
}
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getGames)" << std::endl << root << std::endl;
#endif
if (root["page"].asInt() == root["totalPages"].asInt())
bAllPagesParsed = true;
if (root["products"].isArray())
{
for (unsigned int i = 0; i < root["products"].size(); ++i)
{
Json::Value product = root["products"][i];
gameItem game;
game.name = product["slug"].asString();
game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString();
unsigned int platform = 0;
if (product["worksOn"]["Windows"].asBool())
platform |= GlobalConstants::PLATFORM_WINDOWS;
if (product["worksOn"]["Mac"].asBool())
platform |= GlobalConstants::PLATFORM_MAC;
if (product["worksOn"]["Linux"].asBool())
platform |= GlobalConstants::PLATFORM_LINUX;
// Skip if platform doesn't match
if (config.bPlatformDetection && !(platform & config.iInstallerPlatform))
continue;
// Filter the game list
if (!config.sGameRegex.empty())
{
// GameRegex filter aliases
if (config.sGameRegex == "all")
config.sGameRegex = ".*";
boost::regex expression(config.sGameRegex);
boost::match_results<std::string::const_iterator> what;
if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
continue;
}
if (config.bDLC)
{
int dlcCount = product["dlcCount"].asInt();
bool bDownloadDLCInfo = (dlcCount != 0);
if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty())
{
boost::regex expression(config.sIgnoreDLCCountRegex);
boost::match_results<std::string::const_iterator> what;
if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
{
bDownloadDLCInfo = true;
}
}
// Check game specific config
if (!config.bUpdateCache) // Disable game specific config files for cache update
{
gameSpecificConfig conf;
conf.bIgnoreDLCCount = false; // Assume false
Util::getGameSpecificConfig(game.name, &conf);
if (conf.bIgnoreDLCCount)
bDownloadDLCInfo = true;
}
if (bDownloadDLCInfo && !config.sGameRegex.empty())
{
// don't download unnecessary info if user is only interested in a subset of his account
boost::regex expression(config.sGameRegex);
boost::match_results<std::string::const_iterator> what;
if (!boost::regex_search(game.name, what, expression))
{
bDownloadDLCInfo = false;
}
}
if (bDownloadDLCInfo)
{
game.gamedetailsjson = this->getGameDetailsJSON(game.id);
if (!game.gamedetailsjson.empty())
game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]);
}
}
games.push_back(game);
}
}
i++;
} while (!bAllPagesParsed);
delete jsonparser;
return games;
}
// Get list of free games
std::vector<gameItem> Downloader::getFreeGames()
{
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
std::vector<gameItem> games;
std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title");
// Parse JSON
if (!jsonparser->parse(json, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << json << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getFreeGames)" << std::endl << root << std::endl;
#endif
Json::Value products = root["products"];
for (unsigned int i = 0; i < products.size(); ++i)
{
gameItem game;
game.name = products[i]["slug"].asString();
game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString();
games.push_back(game);
}
delete jsonparser;
return games;
}
Json::Value Downloader::getGameDetailsJSON(const std::string& gameid)
{
std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json";
std::string json = this->getResponse(gameDataUrl);
// Parse JSON
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
if (!jsonparser->parse(json, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << json << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::getGameDetailsJSON)" << std::endl << root << std::endl;
#endif
delete jsonparser;
return root;
}
std::vector<gameFile> Downloader::getExtrasFromJSON(const Json::Value& json, const std::string& gamename)
{
std::vector<gameFile> extras;
@ -3414,141 +2954,37 @@ void Downloader::downloadFileWithId(const std::string& fileid_string, const std:
void Downloader::showWishlist()
{
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
int i = 1;
bool bAllPagesParsed = false;
do
std::vector<wishlistItem> wishlistItems = gogWebsite->getWishlistItems();
for (unsigned int i = 0; i < wishlistItems.size(); ++i)
{
std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i));
// Parse JSON
if (!jsonparser->parse(response, root))
wishlistItem item = wishlistItems[i];
std::string platforms_text = Util::getOptionNameString(item.platform, GlobalConstants::PLATFORMS);
std::string tags_text;
for (unsigned int j = 0; j < item.tags.size(); ++j)
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << response << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
tags_text += (tags_text.empty() ? "" : ", ")+item.tags[j];
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Downloader::showWishlist)" << std::endl << root << std::endl;
#endif
if (root["page"].asInt() >= root["totalPages"].asInt())
bAllPagesParsed = true;
if (root["products"].isArray())
{
for (unsigned int i = 0; i < root["products"].size(); ++i)
{
Json::Value product = root["products"][i];
if (!tags_text.empty())
tags_text = "[" + tags_text + "]";
unsigned int platform = 0;
std::string platforms_text;
bool bIsMovie = product["isMovie"].asBool();
if (!bIsMovie)
{
if (product["worksOn"]["Windows"].asBool())
platform |= GlobalConstants::PLATFORM_WINDOWS;
if (product["worksOn"]["Mac"].asBool())
platform |= GlobalConstants::PLATFORM_MAC;
if (product["worksOn"]["Linux"].asBool())
platform |= GlobalConstants::PLATFORM_LINUX;
std::string price_text = item.price;
if (item.bIsDiscounted)
price_text += " (-" + item.discount_percent + " | -" + item.discount + ")";
// Skip if platform doesn't match
if (config.bPlatformDetection && !(platform & config.iInstallerPlatform))
continue;
platforms_text = Util::getOptionNameString(platform, GlobalConstants::PLATFORMS);
}
std::vector<std::string> tags;
if (product["isComingSoon"].asBool())
tags.push_back("Coming soon");
if (product["isDiscounted"].asBool())
tags.push_back("Discount");
if (bIsMovie)
tags.push_back("Movie");
std::string tags_text;
for (unsigned int j = 0; j < tags.size(); ++j)
{
tags_text += (tags_text.empty() ? "" : ", ")+tags[j];
}
if (!tags_text.empty())
tags_text = "[" + tags_text + "]";
time_t release_date_time;
std::string release_date;
bool bShowReleaseDate = false;
if (product.isMember("releaseDate") && product["isComingSoon"].asBool())
{
if (!product["releaseDate"].empty())
{
if (product["releaseDate"].isInt())
{
release_date_time = product["releaseDate"].asInt();
bShowReleaseDate = true;
}
else
{
std::string release_date_time_string = product["releaseDate"].asString();
if (!release_date_time_string.empty())
{
try
{
release_date_time = std::stoi(release_date_time_string);
bShowReleaseDate = true;
}
catch (std::invalid_argument& e)
{
bShowReleaseDate = false;
}
}
}
}
if (bShowReleaseDate)
release_date = bptime::to_simple_string(bptime::from_time_t(release_date_time));
}
std::string price_text;
std::string currency = product["price"]["symbol"].asString();
std::string price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + currency : product["price"]["finalAmount"].asString() + currency;
std::string discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%";
std::string discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + currency : product["price"]["discountDifference"].asString() + currency;
std::string store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + currency : product["price"]["bonusStoreCreditAmount"].asString() + currency;
price_text = price;
if (product["isDiscounted"].asBool())
price_text += " (-" + discount_percent + " | -" + discount + ")";
std::string url = product["url"].asString();
if (url.find("/game/") == 0)
url = "https://www.gog.com" + url;
else if (url.find("/movie/") == 0)
url = "https://www.gog.com" + url;
std::cout << product["title"].asString();
if (!tags_text.empty())
std::cout << " " << tags_text;
std::cout << std::endl;
std::cout << "\t" << url << std::endl;
if (!bIsMovie)
std::cout << "\tPlatforms: " << platforms_text << std::endl;
if (bShowReleaseDate)
std::cout << "\tRelease date: " << release_date << std::endl;
std::cout << "\tPrice: " << price_text << std::endl;
if (product["price"]["isBonusStoreCreditIncluded"].asBool())
std::cout << "\tStore credit: " << store_credit << std::endl;
std::cout << std::endl;
}
}
i++;
} while (!bAllPagesParsed);
delete jsonparser;
std::cout << item.title;
if (!tags_text.empty())
std::cout << " " << tags_text;
std::cout << std::endl;
std::cout << "\t" << item.url << std::endl;
if (item.platform != 0)
std::cout << "\tPlatforms: " << platforms_text << std::endl;
if (item.release_date_time != 0)
std::cout << "\tRelease date: " << bptime::to_simple_string(bptime::from_time_t(item.release_date_time)) << std::endl;
std::cout << "\tPrice: " << price_text << std::endl;
if (item.bIsBonusStoreCreditIncluded)
std::cout << "\tStore credit: " << item.store_credit << std::endl;
std::cout << std::endl;
}
return;
}

703
src/website.cpp Normal file
View File

@ -0,0 +1,703 @@
/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://www.wtfpl.net/ for more details. */
#include "website.h"
#include "globalconstants.h"
#include <htmlcxx/html/ParserDom.h>
#include <boost/algorithm/string/case_conv.hpp>
Website::Website(Config &conf)
{
this->config = conf;
this->retries = 0;
curlhandle = curl_easy_init();
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, config.sVersionString.c_str());
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, config.iTimeout);
curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, config.sCookiePath.c_str());
curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, config.sCookiePath.c_str());
curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, config.bVerifyPeer);
curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, config.bVerbose);
curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, config.iDownloadRate);
// Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30);
curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
}
Website::~Website()
{
curl_easy_cleanup(curlhandle);
}
size_t Website::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp)
{
std::ostringstream *stream = (std::ostringstream*)userp;
size_t count = size * nmemb;
stream->write(ptr, count);
return count;
}
std::string Website::getResponse(const std::string& url)
{
std::ostringstream memory;
std::string response;
curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
CURLcode result;
do
{
if (config.iWait > 0)
usleep(config.iWait); // Delay the request by specified time
result = curl_easy_perform(curlhandle);
response = memory.str();
memory.str(std::string());
}
while ((result != CURLE_OK) && response.empty() && (this->retries++ < config.iRetries));
this->retries = 0; // reset retries counter
if (result != CURLE_OK)
{
std::cout << curl_easy_strerror(result) << std::endl;
if (result == CURLE_HTTP_RETURNED_ERROR)
{
long int response_code = 0;
result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
std::cout << "HTTP ERROR: ";
if (result == CURLE_OK)
std::cout << response_code << " (" << url << ")" << std::endl;
else
std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
}
}
return response;
}
Json::Value Website::getGameDetailsJSON(const std::string& gameid)
{
std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json";
std::string json = this->getResponse(gameDataUrl);
// Parse JSON
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
if (!jsonparser->parse(json, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << json << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << root << std::endl;
#endif
delete jsonparser;
return root;
}
// Get list of games from account page
std::vector<gameItem> Website::getGames()
{
std::vector<gameItem> games;
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
int i = 1;
bool bAllPagesParsed = false;
do
{
std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
// Parse JSON
if (!jsonparser->parse(response, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << response << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
if (!response.empty())
{
if(response[0] != '{')
{
// Response was not JSON. Assume that cookies have expired.
std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl;
}
}
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << root << std::endl;
#endif
if (root["page"].asInt() == root["totalPages"].asInt())
bAllPagesParsed = true;
if (root["products"].isArray())
{
for (unsigned int i = 0; i < root["products"].size(); ++i)
{
Json::Value product = root["products"][i];
gameItem game;
game.name = product["slug"].asString();
game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString();
unsigned int platform = 0;
if (product["worksOn"]["Windows"].asBool())
platform |= GlobalConstants::PLATFORM_WINDOWS;
if (product["worksOn"]["Mac"].asBool())
platform |= GlobalConstants::PLATFORM_MAC;
if (product["worksOn"]["Linux"].asBool())
platform |= GlobalConstants::PLATFORM_LINUX;
// Skip if platform doesn't match
if (config.bPlatformDetection && !(platform & config.iInstallerPlatform))
continue;
// Filter the game list
if (!config.sGameRegex.empty())
{
// GameRegex filter aliases
if (config.sGameRegex == "all")
config.sGameRegex = ".*";
boost::regex expression(config.sGameRegex);
boost::match_results<std::string::const_iterator> what;
if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
continue;
}
if (config.bDLC)
{
int dlcCount = product["dlcCount"].asInt();
bool bDownloadDLCInfo = (dlcCount != 0);
if (!bDownloadDLCInfo && !config.sIgnoreDLCCountRegex.empty())
{
boost::regex expression(config.sIgnoreDLCCountRegex);
boost::match_results<std::string::const_iterator> what;
if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
{
bDownloadDLCInfo = true;
}
}
// Check game specific config
if (!config.bUpdateCache) // Disable game specific config files for cache update
{
gameSpecificConfig conf;
conf.bIgnoreDLCCount = false; // Assume false
Util::getGameSpecificConfig(game.name, &conf);
if (conf.bIgnoreDLCCount)
bDownloadDLCInfo = true;
}
if (bDownloadDLCInfo && !config.sGameRegex.empty())
{
// don't download unnecessary info if user is only interested in a subset of his account
boost::regex expression(config.sGameRegex);
boost::match_results<std::string::const_iterator> what;
if (!boost::regex_search(game.name, what, expression))
{
bDownloadDLCInfo = false;
}
}
if (bDownloadDLCInfo)
{
game.gamedetailsjson = this->getGameDetailsJSON(game.id);
if (!game.gamedetailsjson.empty())
game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]);
}
}
games.push_back(game);
}
}
i++;
} while (!bAllPagesParsed);
delete jsonparser;
return games;
}
// Get list of free games
std::vector<gameItem> Website::getFreeGames()
{
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
std::vector<gameItem> games;
std::string json = this->getResponse("https://www.gog.com/games/ajax/filtered?mediaType=game&page=1&price=free&sort=title");
// Parse JSON
if (!jsonparser->parse(json, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << json << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getFreeGames)" << std::endl << root << std::endl;
#endif
Json::Value products = root["products"];
for (unsigned int i = 0; i < products.size(); ++i)
{
gameItem game;
game.name = products[i]["slug"].asString();
game.id = products[i]["id"].isInt() ? std::to_string(products[i]["id"].asInt()) : products[i]["id"].asString();
games.push_back(game);
}
delete jsonparser;
return games;
}
// Login to GOG website
int Website::Login(const std::string& email, const std::string& password)
{
int res = 0;
std::string postdata;
std::ostringstream memory;
std::string token;
std::string tagname_username;
std::string tagname_password;
std::string tagname_login;
std::string tagname_token;
// Get login token
std::string html = this->getResponse("https://www.gog.com/");
htmlcxx::HTML::ParserDom parser;
tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
tree<htmlcxx::HTML::Node>::iterator end = dom.end();
// Find auth_url
bool bFoundAuthUrl = false;
for (; it != end; ++it)
{
if (it->tagName()=="script")
{
std::string auth_url;
for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
{
tree<htmlcxx::HTML::Node>::iterator script_it = dom.child(it, i);
if (!script_it->isTag() && !script_it->isComment())
{
if (script_it->text().find("GalaxyAccounts") != std::string::npos)
{
boost::match_results<std::string::const_iterator> what;
boost::regex expression(".*'(https://auth.gog.com/.*?)'.*");
boost::regex_match(script_it->text(), what, expression);
auth_url = what[1];
break;
}
}
}
if (!auth_url.empty())
{ // Found auth_url, get the necessary info for login
bFoundAuthUrl = true;
std::string login_form_html = this->getResponse(auth_url);
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::Login)" << std::endl;
std::cerr << login_form_html << std::endl;
#endif
if (login_form_html.find("google.com/recaptcha") != std::string::npos)
{
std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl
<< "Login with browser and export cookies to \"" << config.sCookiePath << "\"" << std::endl;
return res = 0;
}
tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end();
for (; login_it != login_it_end; ++login_it)
{
if (login_it->tagName()=="input")
{
login_it->parseAttributes();
std::string id_login = login_it->attribute("id").second;
if (id_login == "login_username")
{
tagname_username = login_it->attribute("name").second;
}
else if (id_login == "login_password")
{
tagname_password = login_it->attribute("name").second;
}
else if (id_login == "login__token")
{
token = login_it->attribute("value").second; // login token
tagname_token = login_it->attribute("name").second;
}
}
else if (login_it->tagName()=="button")
{
login_it->parseAttributes();
std::string id_login = login_it->attribute("id").second;
if (id_login == "login_login")
{
tagname_login = login_it->attribute("name").second;
}
}
}
break;
}
}
}
if (!bFoundAuthUrl)
{
std::cout << "Failed to find url for login form" << std::endl;
}
if (token.empty())
{
std::cout << "Failed to get login token" << std::endl;
return res = 0;
}
//Create postdata - escape characters in email/password to support special characters
postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size())
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size())
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "="
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size());
curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check");
curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
// Don't follow to redirect location because we need to check it for two step authorization.
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
CURLcode result = curl_easy_perform(curlhandle);
memory.str(std::string());
if (result != CURLE_OK)
{
// Expected to hit maximum amount of redirects so don't print error on it
if (result != CURLE_TOO_MANY_REDIRECTS)
std::cout << curl_easy_strerror(result) << std::endl;
}
// Get redirect url
char *redirect_url;
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
// Handle two step authorization
if (std::string(redirect_url).find("two_step") != std::string::npos)
{
std::string security_code, tagname_two_step_send, tagname_two_step_auth_letter_1, tagname_two_step_auth_letter_2, tagname_two_step_auth_letter_3, tagname_two_step_auth_letter_4, tagname_two_step_token, token_two_step;
std::string two_step_html = this->getResponse(redirect_url);
redirect_url = NULL;
tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end();
for (; two_step_it != two_step_it_end; ++two_step_it)
{
if (two_step_it->tagName()=="input")
{
two_step_it->parseAttributes();
std::string id_two_step = two_step_it->attribute("id").second;
if (id_two_step == "second_step_authentication_token_letter_1")
{
tagname_two_step_auth_letter_1 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_2")
{
tagname_two_step_auth_letter_2 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_3")
{
tagname_two_step_auth_letter_3 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication_token_letter_4")
{
tagname_two_step_auth_letter_4 = two_step_it->attribute("name").second;
}
else if (id_two_step == "second_step_authentication__token")
{
token_two_step = two_step_it->attribute("value").second; // two step token
tagname_two_step_token = two_step_it->attribute("name").second;
}
}
else if (two_step_it->tagName()=="button")
{
two_step_it->parseAttributes();
std::string id_two_step = two_step_it->attribute("id").second;
if (id_two_step == "second_step_authentication_send")
{
tagname_two_step_send = two_step_it->attribute("name").second;
}
}
}
std::cerr << "Security code: ";
std::getline(std::cin,security_code);
if (security_code.size() != 4)
{
std::cerr << "Security code must be 4 characters long" << std::endl;
exit(1);
}
postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3]
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "="
+ "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size());
curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step");
curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
// Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
result = curl_easy_perform(curlhandle);
memory.str(std::string());
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
}
curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1);
curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1);
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
result = curl_easy_perform(curlhandle);
if (result != CURLE_OK)
{
std::cout << curl_easy_strerror(result) << std::endl;
}
if (this->IsLoggedInComplex(email))
{
res = 1; // Login was successful
}
else
{
if (this->IsloggedInSimple())
res = 1; // Login was successful
}
if (res == 1)
{
curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, "FLUSH"); // Write all known cookies to the file specified by CURLOPT_COOKIEJAR
}
return res;
}
bool Website::IsLoggedIn()
{
return this->IsloggedInSimple();
}
/* Complex login check. Check login by checking email address on the account settings page.
returns true if we are logged in
returns false if we are not logged in
*/
bool Website::IsLoggedInComplex(const std::string& email)
{
bool bIsLoggedIn = false;
std::string html = this->getResponse("https://www.gog.com/account/settings/personal");
std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it
htmlcxx::HTML::ParserDom parser;
tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
tree<htmlcxx::HTML::Node>::iterator end = dom.end();
dom = parser.parseTree(html);
it = dom.begin();
end = dom.end();
for (; it != end; ++it)
{
if (it->tagName()=="strong")
{
it->parseAttributes();
if (it->attribute("class").second == "settings-item__value settings-item__section")
{
for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
{
tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i);
if (!tag_it->isTag() && !tag_it->isComment())
{
std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text());
if (tag_text == email_lowercase)
{
bIsLoggedIn = true; // We are logged in
break;
}
}
}
}
}
if (bIsLoggedIn) // We are logged in so no need to go through the remaining tags
break;
}
return bIsLoggedIn;
}
/* Simple login check. Check login by trying to get account page. If response code isn't 200 then login failed.
returns true if we are logged in
returns false if we are not logged in
*/
bool Website::IsloggedInSimple()
{
bool bIsLoggedIn = false;
std::ostringstream memory;
std::string url = "https://www.gog.com/account";
long int response_code = 0;
curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
curl_easy_perform(curlhandle);
memory.str(std::string());
curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
if (response_code == 200)
bIsLoggedIn = true; // We are logged in
return bIsLoggedIn;
}
std::vector<wishlistItem> Website::getWishlistItems()
{
Json::Value root;
Json::Reader *jsonparser = new Json::Reader;
int i = 1;
bool bAllPagesParsed = false;
std::vector<wishlistItem> wishlistItems;
do
{
std::string response = this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i));
// Parse JSON
if (!jsonparser->parse(response, root))
{
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << response << std::endl;
#endif
std::cout << jsonparser->getFormattedErrorMessages();
delete jsonparser;
exit(1);
}
#ifdef DEBUG
std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << root << std::endl;
#endif
if (root["page"].asInt() >= root["totalPages"].asInt())
bAllPagesParsed = true;
if (root["products"].isArray())
{
for (unsigned int i = 0; i < root["products"].size(); ++i)
{
wishlistItem item;
Json::Value product = root["products"][i];
item.platform = 0;
std::string platforms_text;
bool bIsMovie = product["isMovie"].asBool();
if (!bIsMovie)
{
if (product["worksOn"]["Windows"].asBool())
item.platform |= GlobalConstants::PLATFORM_WINDOWS;
if (product["worksOn"]["Mac"].asBool())
item.platform |= GlobalConstants::PLATFORM_MAC;
if (product["worksOn"]["Linux"].asBool())
item.platform |= GlobalConstants::PLATFORM_LINUX;
// Skip if platform doesn't match
if (config.bPlatformDetection && !(item.platform & config.iInstallerPlatform))
continue;
}
if (product["isComingSoon"].asBool())
item.tags.push_back("Coming soon");
if (product["isDiscounted"].asBool())
item.tags.push_back("Discount");
if (bIsMovie)
item.tags.push_back("Movie");
item.release_date_time = 0;
if (product.isMember("releaseDate") && product["isComingSoon"].asBool())
{
if (!product["releaseDate"].empty())
{
if (product["releaseDate"].isInt())
{
item.release_date_time = product["releaseDate"].asInt();
}
else
{
std::string release_date_time_string = product["releaseDate"].asString();
if (!release_date_time_string.empty())
{
try
{
item.release_date_time = std::stoi(release_date_time_string);
}
catch (std::invalid_argument& e)
{
item.release_date_time = 0;
}
}
}
}
}
item.currency = product["price"]["symbol"].asString();
item.price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + item.currency : product["price"]["finalAmount"].asString() + item.currency;
item.discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%";
item.discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + item.currency : product["price"]["discountDifference"].asString() + item.currency;
item.store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + item.currency : product["price"]["bonusStoreCreditAmount"].asString() + item.currency;
item.url = product["url"].asString();
if (item.url.find("/game/") == 0)
item.url = "https://www.gog.com" + item.url;
else if (item.url.find("/movie/") == 0)
item.url = "https://www.gog.com" + item.url;
item.title = product["title"].asString();
item.bIsBonusStoreCreditIncluded = product["price"]["isBonusStoreCreditIncluded"].asBool();
item.bIsDiscounted = product["isDiscounted"].asBool();
wishlistItems.push_back(item);
}
}
i++;
} while (!bAllPagesParsed);
delete jsonparser;
return wishlistItems;
}