From 1c49f0d699de1bf7a8fa015351a22c68321e447c Mon Sep 17 00:00:00 2001 From: Nis Wechselberg Date: Sat, 3 Jun 2017 13:53:04 +0200 Subject: [PATCH] Added notification script --- .gitignore | 137 ++++++++++++++++++++++++ gogconnect-notify.py | 250 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 .gitignore create mode 100755 gogconnect-notify.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87250c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +gog-cookies.dat + +# Created by https://www.gitignore.io/api/python,sublimetext + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +# End of https://www.gitignore.io/api/python,sublimetext diff --git a/gogconnect-notify.py b/gogconnect-notify.py new file mode 100755 index 0000000..37fa440 --- /dev/null +++ b/gogconnect-notify.py @@ -0,0 +1,250 @@ +#!/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'http://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) + + + # 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": + 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)