#!/usr/bin/env python # -*- coding: utf-8 -*- # pylint: disable= I0011,C0103 from __future__ import print_function from __future__ import division from __future__ import unicode_literals # imports import sys import logging import contextlib import json import getpass import argparse import socket import html5lib import requests # python 3 import http.cookiejar as cookiejar from http.client import BadStatusLine from urllib.parse import urlencode from urllib.request import HTTPCookieProcessor, HTTPError, URLError, build_opener, Request __appname__ = 'gogconnect-notify.py' __author__ = 'eNBeWe' __version__ = '0.1' __url__ = 'https://github.com/eNBeWe/gogconnect-notify' # configure logging logFormatter = logging.Formatter("%(asctime)s | %(message)s", datefmt='%H:%M:%S') rootLogger = logging.getLogger('ws') rootLogger.setLevel(logging.DEBUG) consoleHandler = logging.StreamHandler(sys.stdout) consoleHandler.setFormatter(logFormatter) rootLogger.addHandler(consoleHandler) # filepath constants COOKIES_FILENAME = r'gog-cookies.dat' # global web utilities global_cookies = cookiejar.LWPCookieJar(COOKIES_FILENAME) cookieproc = HTTPCookieProcessor(global_cookies) opener = build_opener(cookieproc) # GOG URLs GOG_HOME_URL = r'https://www.gog.com' GOG_LOGIN_URL = r'https://login.gog.com/login_check' GOG_API_URL = r'https://www.gog.com/api/v1/users/' # HTTP request settings HTTP_RETRY_COUNT = 3 HTTP_PERM_ERRORCODES = (404, 403, 503) def request(url, args=None, retries=HTTP_RETRY_COUNT): """Performs web request to url with optional retries, delay, and byte range. """ _retry = False try: if args is not None: enc_args = urlencode(args) enc_args = enc_args.encode('ascii') # needed for Python 3 else: enc_args = None req = Request(url, data=enc_args) page = opener.open(req) except (HTTPError, URLError, socket.error, BadStatusLine) as e: if isinstance(e, HTTPError): if e.code in HTTP_PERM_ERRORCODES: # do not retry these HTTP codes rootLogger.warn('request failed: %s. will not retry.', e) raise if retries > 0: _retry = True else: raise if _retry: rootLogger.warn('request failed: %s (%d retries left)', e, retries) return request(url=url, args=args, retries=retries-1) return contextlib.closing(page) def load_cookies(): # try to load as default lwp format try: global_cookies.load() return except IOError: pass rootLogger.error('failed to load cookies, did you login first?') raise SystemExit(1) def process_argv(argv): p1 = argparse.ArgumentParser(description='%s (%s)' % (__appname__, __url__), add_help=False) sp1 = p1.add_subparsers(help='commands', dest='cmd', title='commands') g1 = sp1.add_parser('login', help='Login to GOG and save a local copy of your authenticated cookie') g1.add_argument('username', action='store', help='GOG username/email', nargs='?', default=None) g1.add_argument('password', action='store', help='GOG password', nargs='?', default=None) g1 = sp1.add_parser('check', help='Check for new games in GOG connect') g1 = p1.add_argument_group('other') g1.add_argument('-h', '--help', action='help', help='show help message and exit') g1.add_argument('-v', '--version', action='version', help='show version number and exit', version="%s (version %s)" % (__appname__, __version__)) # parse the given argv. raises SystemExit on error args = p1.parse_args(argv[1:]) return args # -------- # Commands # -------- def cmd_login(user, passwd): """Attempts to log into GOG and saves the resulting cookiejar to disk. """ login_data = {'user': user, 'passwd': passwd, 'auth_url': None, 'login_token': None, 'two_step_url': None, 'two_step_token': None, 'two_step_security_code': None, 'login_success': False, } global_cookies.clear() # reset cookiejar # prompt for login/password if needed if login_data['user'] is None: login_data['user'] = input("Username: ") if login_data['passwd'] is None: login_data['passwd'] = getpass.getpass() rootLogger.info("attempting gog login as '%s' ...", login_data['user']) # fetch the auth url with request(GOG_HOME_URL) as page: etree = html5lib.parse(page, namespaceHTMLElements=False) for elm in etree.findall('.//script'): if elm.text is not None and 'GalaxyAccounts' in elm.text: login_data['auth_url'] = elm.text.split("'")[1] break # fetch the login token with request(login_data['auth_url']) as page: etree = html5lib.parse(page, namespaceHTMLElements=False) # Bail if we find a request for a reCAPTCHA if len(etree.findall('.//div[@class="g-recaptcha"]')) > 0: rootLogger.error( "cannot continue, gog is asking for a reCAPTCHA :( try again in a few minutes.") return for elm in etree.findall('.//input'): if elm.attrib['id'] == 'login__token': login_data['login_token'] = elm.attrib['value'] break # perform login and capture two-step token if required with request(GOG_LOGIN_URL, args={'login[username]': login_data['user'], 'login[password]': login_data['passwd'], 'login[login]': '', 'login[_token]': login_data['login_token']}) as page: etree = html5lib.parse(page, namespaceHTMLElements=False) if 'two_step' in page.geturl(): login_data['two_step_url'] = page.geturl() for elm in etree.findall('.//input'): if elm.attrib['id'] == 'second_step_authentication__token': login_data['two_step_token'] = elm.attrib['value'] break elif 'on_login_success' in page.geturl(): login_data['login_success'] = True # perform two-step if needed if login_data['two_step_url'] is not None: login_data['two_step_security_code'] = input("enter two-step security code: ") # Send the security code back to GOG with request(login_data['two_step_url'], args={'second_step_authentication[token][letter_1]': login_data['two_step_security_code'][0], 'second_step_authentication[token][letter_2]': login_data['two_step_security_code'][1], 'second_step_authentication[token][letter_3]': login_data['two_step_security_code'][2], 'second_step_authentication[token][letter_4]': login_data['two_step_security_code'][3], 'second_step_authentication[send]': "", 'second_step_authentication[_token]': login_data['two_step_token'] }) as page: if 'on_login_success' in page.geturl(): login_data['login_success'] = True # save cookies on success if login_data['login_success']: rootLogger.info('login successful!') global_cookies.save() else: rootLogger.error('login failed, verify your username/password and try again.') def cmd_check(): load_cookies() session = requests.Session() session.headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0" session.cookies = global_cookies user_data = json.loads(session.get("{}/userData.json".format(GOG_HOME_URL)).text) # TODO: Make sure the user is logged in (check isLoggedIn in received JSON) # Refresh Steam products refresh_url = "{}/{}/gogLink/steam/synchronizeUserProfile".format(GOG_API_URL, user_data["userId"]) session.get(refresh_url) steam_products_url = "{}/{}/gogLink/steam/exchangeableProducts".format(GOG_API_URL, user_data["userId"]) steam_products = json.loads(session.get(steam_products_url).text) games_available = False if len(steam_products["items"]) > 0: for key, value in steam_products["items"].items(): if value["status"] == "available" or value["status"] == "readyToLink": games_available = True break if games_available: print("New games available!") def main(args): if args.cmd == 'login': cmd_login(args.username, args.password) elif args.cmd == 'check': cmd_check() if __name__ == "__main__": try: main(process_argv(sys.argv)) except KeyboardInterrupt: sys.exit(1) except SystemExit: raise except: rootLogger.exception('fatal...') sys.exit(1)