diff options
Diffstat (limited to 'pkgs/games')
-rw-r--r-- | pkgs/games/default.nix | 2 | ||||
-rw-r--r-- | pkgs/games/gog/default.nix | 39 | ||||
-rw-r--r-- | pkgs/games/gog/fetch-gog/default.nix | 243 |
3 files changed, 283 insertions, 1 deletions
diff --git a/pkgs/games/default.nix b/pkgs/games/default.nix index dd97148d..ae86d30e 100644 --- a/pkgs/games/default.nix +++ b/pkgs/games/default.nix @@ -42,7 +42,7 @@ let packages = (pkgs.lib.evalModules { modules = [ (if config == null then configFile else config) - baseModule ./humblebundle ./steam ./itch + baseModule ./humblebundle ./steam ./itch ./gog ]; }).config.packages; diff --git a/pkgs/games/gog/default.nix b/pkgs/games/gog/default.nix new file mode 100644 index 00000000..111091c3 --- /dev/null +++ b/pkgs/games/gog/default.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.gog; + + self = rec { + callPackage = pkgs.lib.callPackageWith (pkgs // self); + callPackage_i686 = pkgs.lib.callPackageWith (pkgs.pkgsi686Linux // self); + + fetchGog = callPackage ./fetch-gog { + inherit (config.gog) email password; + }; + }; +in { + options.gog = { + email = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Email address for your GOG account. + ''; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Password for your GOG account. + + <note><para>This will end up in the Nix store and other users on the + same machine can read it!</para></note> + ''; + }; + }; + + config.packages = { + gog = lib.mkIf (cfg.email != null && cfg.password != null) self; + }; +} diff --git a/pkgs/games/gog/fetch-gog/default.nix b/pkgs/games/gog/fetch-gog/default.nix new file mode 100644 index 00000000..3389eb60 --- /dev/null +++ b/pkgs/games/gog/fetch-gog/default.nix @@ -0,0 +1,243 @@ +{ stdenv, lib, curl, writeText, runCommandCC, python3Packages, cacert +, pkgconfig, qt5 + +, email, password +}: + +{ productId, downloadName, sha256, downloadType ? "installer", suffix ? "sh" +, name ? "${toString productId}-${downloadName}-${downloadType}.${suffix}" +}: + +let + getCaptcha = let + mkCString = val: let + escaped = lib.replaceStrings ["\"" "\\" "\n"] ["\\\"" "\\\\" "\\n"] val; + in "\"${escaped}\""; + + injectedJS = '' + var user_input = document.getElementById('login_username'); + var pass_input = document.getElementById('login_password'); + var submit_button = document.getElementById('login_login'); + + user_input.value = ${builtins.toJSON email}; + user_input.style.display = 'none'; + pass_input.value = ${builtins.toJSON password}; + pass_input.style.display = 'none'; + + submit_button.style.display = 'none'; + + function waitForResponse() { + var response = grecaptcha.getResponse(); + if (response != "") + submit_button.click(); + else + setTimeout(waitForResponse, 50); + } + + waitForResponse(); + ''; + + application = writeText "captcha.cc" '' + #include <QApplication> + #include <QWebEngineView> + #include <QTcpServer> + #include <QQuickWebEngineProfile> + #include <QUrlQuery> + + // Taken from lgogdownloader (https://github.com/Sude-/lgogdownloader): + static QString clientId = "46899977096215655"; + static QString clientSecret = + "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"; + static QString redirectUri = + "https://embed.gog.com/on_login_success?origin=client"; + + static QUrl getAuthUrl() { + QUrl url("https://auth.gog.com/auth"); + + QUrlQuery query; + query.addQueryItem("client_id", clientId); + query.addQueryItem("redirect_uri", redirectUri); + query.addQueryItem("response_type", "code"); + query.addQueryItem("layout", "default"); + query.addQueryItem("brand", "gog"); + + url.setQuery(query); + return url; + } + + static QUrl getTokenUrl(const QString &code) { + QUrl url("https://auth.gog.com/token"); + + QUrlQuery query; + query.addQueryItem("client_id", clientId); + query.addQueryItem("client_secret", clientSecret); + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", code); + query.addQueryItem("redirect_uri", redirectUri); + + url.setQuery(query); + return url; + } + + int main(int argc, char **argv) { + QApplication *app = new QApplication(argc, argv); + QTcpServer *server = new QTcpServer(); + QWebEngineView *browser = new QWebEngineView(); + + QQuickWebEngineProfile::defaultProfile()->setOffTheRecord(true); + + if (!server->listen(QHostAddress::LocalHost, 18321)) { + qCritical() << "Unable to listen on port 18321!"; + return 1; + } + + qInfo() << "Waiting for connection from the GOG login helper..."; + if (!server->waitForNewConnection(-1)) { + qCritical() << "Unable to accept the connection!"; + return 1; + } + qInfo() << "Connection established, spawning window to login."; + + QTcpSocket *sock = server->nextPendingConnection(); + + browser->load(getAuthUrl()); + browser->show(); + + browser->connect(browser, &QWebEngineView::loadFinished, [=]() { + browser->page()->runJavaScript(${mkCString injectedJS}); + browser->connect( + browser, &QWebEngineView::urlChanged, [=](const QUrl &newurl + ) { + QUrlQuery newquery(newurl.query()); + QString code = newquery.queryItemValue("code"); + sock->write(getTokenUrl(code).toEncoded()); + sock->flush(); + sock->waitForBytesWritten(); + sock->close(); + server->close(); + app->quit(); + }); + }); + + return app->exec(); + } + ''; + + in runCommandCC "get-captcha" { + nativeBuildInputs = [ pkgconfig ]; + buildInputs = [ qt5.qtbase qt5.qtwebengine ]; + } '' + g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \ + -Wall -std=c++11 -o "$out" ${application} + ''; + + mkPyStr = str: "'${stdenv.lib.escape ["'" "\\"] (toString str)}'"; + + fetcher = writeText "fetch-gog.py" '' + import sys, socket, time + from urllib.request import urlopen, Request + from urllib.error import HTTPError + from json import loads + + from tabulate import tabulate + + class GogFetcher: + def __init__(self, product_id, download_type, download_name): + sys.stderr.write("Solving a captcha is required to log in.\n") + sys.stderr.write("Please run " ${mkPyStr getCaptcha} " now.\n") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sys.stderr.write("Waiting for connection") + i = 0 + while sock.connect_ex(("127.0.0.1", 18321)) != 0: + time.sleep(0.1) + if i % 10 == 0: + sys.stderr.write('.') + sys.stderr.flush() + i += 1 + sys.stderr.write(" connected.\n") + sys.stderr.write("Waiting for captcha to be solved...\n") + token_url = sock.recv(4096) + sock.close() + sys.stderr.write("Captcha solved correctly, logging in.\n") + response = urlopen( + token_url.decode(), + cafile=${mkPyStr "${cacert}/etc/ssl/certs/ca-bundle.crt"} + ) + + self.product_id = product_id + self.download_type = download_type + self.download_name = download_name + self.access_token = loads(response.read())['access_token'] + + def request(self, url): + headers = {"Authorization": "Bearer " + self.access_token} + return loads(urlopen( + Request(url, headers=headers), + cafile=${mkPyStr "${cacert}/etc/ssl/certs/ca-bundle.crt"} + ).read()) + + def strip_dtype(self, dtype): + if dtype.endswith('es'): + return dtype[:-2] + elif dtype.endswith('s'): + return dtype[:-1] + else: + return dtype + + def list_downloads(self): + url = "https://api.gog.com/products/" + self.product_id \ + + "?expand=downloads" + downloads = self.request(url)['downloads'] + table = [] + for dtype, dloads in downloads.items(): + for dload in dloads: + for dlfile in dload['files']: + table.append([ + self.strip_dtype(dtype), dload.get('os', ""), + dload.get('language_full', ""), + dlfile['id'], dlfile['size'], dlfile['downlink'] + ]) + sys.stderr.write(tabulate(table, headers=[ + 'Download type', + 'Operating system', + 'Language', + 'Identifier', + 'Size', + 'URL' + ]) + "\n") + + def fetch(self): + url = "http://api.gog.com/products/" + self.product_id \ + + "/downlink/" + self.download_type + "/" + self.download_name + try: + download = self.request(url) + except HTTPError: + m = "Download {!r} with type {!r} not found.\nValid downloads are:\n" + sys.stderr.write(m.format(self.download_name, self.download_type)) + self.list_downloads() + raise SystemExit(1) + else: + print(download['downlink']) + + GogFetcher(sys.argv[1], sys.argv[2], sys.argv[3]).fetch() + ''; + +in stdenv.mkDerivation { + inherit name; + outputHashAlgo = "sha256"; + outputHash = sha256; + + nativeBuildInputs = [ curl python3Packages.tabulate ]; + + buildCommand = '' + url="$(${python3Packages.python.interpreter} ${fetcher} \ + ${toString productId} \ + ${lib.escapeShellArg downloadType} \ + ${lib.escapeShellArg downloadName})" + header "downloading $name from $url" + curl \ + --cacert ${lib.escapeShellArg "${cacert}/etc/ssl/certs/ca-bundle.crt"} \ + --fail --output "$out" "$url" + stopNest + ''; +} |