Added notification script
This commit is contained in:
parent
b5060dea67
commit
1c49f0d699
2 changed files with 387 additions and 0 deletions
137
.gitignore
vendored
Normal file
137
.gitignore
vendored
Normal file
|
@ -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
|
250
gogconnect-notify.py
Executable file
250
gogconnect-notify.py
Executable file
|
@ -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)
|
Loading…
Reference in a new issue