Skip to content

Commit 99ac6c3

Browse files
version 0.1
1 parent 40647f6 commit 99ac6c3

File tree

11 files changed

+452
-0
lines changed

11 files changed

+452
-0
lines changed

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
python_paths = src
3+
addopts = src tests --flakes

requirements/app.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
galaxy.plugin.api==0.53
2+
python-dateutil==2.8.0
3+
requests==2.21.0
4+
psutil==5.6.1

requirements/dev.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-r app.txt
2+
invoke==1.2.0
3+
pytest==4.3.0
4+
pytest-flakes==4.0.0
5+
pytest-pythonpath==0.7.3
6+
pytest-asyncio==0.10.0
7+
pytest-mock==1.10.4
8+
pip-tools==3.6.1
9+
aiohttp==3.5.4

src/backend.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging as log
2+
import asyncio
3+
4+
5+
class ParadoxClient:
6+
def __init__(self, http_client):
7+
self.http_client = http_client
8+
self.paradox_launcher_skus = None
9+
10+
async def prepare_sku(self):
11+
data = {'token': self.http_client.token}
12+
log.info('Starting skus retrieve')
13+
response = await self.http_client.do_request('GET', 'https://accounts.paradoxplaza.com/api/skus', headers=data)
14+
response = await response.json()
15+
log.info('Finished skus retrieve')
16+
paradox_launcher_skus = set()
17+
for sku in response:
18+
await asyncio.sleep(0.01)
19+
if 'paradoxLauncher' in response[sku]['platform']:
20+
paradox_launcher_skus.add(sku)
21+
self.paradox_launcher_skus = paradox_launcher_skus
22+
23+
async def get_account_id(self):
24+
data = {'Authorization': f'{{"session":{{"token":"{self.http_client.token}"}}}}',
25+
'content-type': 'application/json'}
26+
response = await self.http_client.do_request('GET', 'https://api.paradox-interactive.com/accounts', headers=data)
27+
response = await response.json()
28+
return response['id']
29+
30+
async def get_owned_games(self):
31+
data = {'Authorization': f'{{"session":{{"token":"{self.http_client.token}"}}}}',
32+
'content-type': 'application/json'}
33+
response = await self.http_client.do_request('GET', 'https://api.paradox-interactive.com/inventory/products',
34+
headers=data)
35+
36+
response = await response.json()
37+
owned_products = []
38+
if 'products' in response:
39+
for platforms in response['products']:
40+
for platform in platforms:
41+
for game in platforms[platform]:
42+
log.info(game)
43+
if game['sku'] and game['title'] and game['product_type']:
44+
owned_products.append({'sku': game['sku'],
45+
'title': game['title'],
46+
'type': game['product_type']})
47+
log.info(owned_products)
48+
return owned_products

src/consts.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
import re
3+
4+
5+
AUTH_URL = r"https://accounts.paradoxplaza.com/login"
6+
AUTH_REDIRECT_URL = r"api/accounts/connections/"
7+
8+
REGISTRY_LAUNCHER_PATH = r"SOFTWARE\WOW6432Node\Paradox Interactive\Paradox Launcher\LauncherPath"
9+
PARADOX_LAUNCHER_EXE = "Paradox Launcher.exe"
10+
11+
12+
def regex_pattern(regex):
13+
return ".*" + re.escape(regex) + ".*"
14+
15+
AUTH_PARAMS = {
16+
"window_title": "Login to Paradox\u2122",
17+
"window_width": 700,
18+
"window_height": 800,
19+
"start_uri": AUTH_URL,
20+
"end_uri_regex": regex_pattern(AUTH_REDIRECT_URL)
21+
}
22+
23+

src/http_client.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
from galaxy.http import HttpClient
3+
4+
import aiohttp
5+
import logging as log
6+
from yarl import URL
7+
import pickle
8+
9+
class CookieJar(aiohttp.CookieJar):
10+
def __init__(self):
11+
super().__init__()
12+
self._cookies_updated_callback = None
13+
14+
def set_cookies_updated_callback(self, callback):
15+
self._cookies_updated_callback = callback
16+
17+
def update_cookies(self, cookies, url=URL()):
18+
super().update_cookies(cookies, url)
19+
if cookies and self._cookies_updated_callback:
20+
self._cookies_updated_callback(list(self))
21+
22+
23+
class AuthenticatedHttpClient(HttpClient):
24+
25+
def __init__(self, store_credentials):
26+
self._store_credentials = store_credentials
27+
self.token = None
28+
29+
self.bearer = None
30+
self.user = None
31+
self._cookie_jar = CookieJar()
32+
self._auth_lost_callback = None
33+
34+
super().__init__(cookie_jar=self._cookie_jar)
35+
36+
def set_cookies_updated_callback(self, callback):
37+
self._cookie_jar.set_cookies_updated_callback(callback)
38+
39+
def update_cookies(self, cookies):
40+
self._cookie_jar.update_cookies(cookies)
41+
42+
def set_auth_lost_callback(self, callback):
43+
self._auth_lost_callback = callback
44+
45+
46+
def get_credentials(self):
47+
creds = {}
48+
creds['cookie_jar'] = pickle.dumps([c for c in self._cookie_jar]).hex()
49+
return creds
50+
51+
async def do_request(self, method, *args, **kwargs):
52+
try:
53+
return await self.request(method, *args, **kwargs)
54+
except Exception as e:
55+
log.warning(f"Request failed with {repr(e)}, attempting to refresh credentials")
56+
#await self.refresh_credentials()
57+
return await self.request(method, *args, **kwargs)
58+
59+
def authenticate_with_cookies(self, cookies):
60+
cookiez = {}
61+
for cookie in cookies:
62+
if 'value' in cookie:
63+
cookiez[cookie['name']] = cookie['value']
64+
else:
65+
cookiez[cookie.key] = cookie.value
66+
self.update_cookies(cookiez)
67+
self.token = cookiez['SESSION_TOKEN']
68+
self._store_credentials(self.get_credentials())
69+
70+
71+
72+
73+
74+

src/local.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import sys
2+
if sys.platform == 'win32':
3+
import winreg
4+
5+
import os
6+
import logging as log
7+
import asyncio
8+
from consts import REGISTRY_LAUNCHER_PATH, PARADOX_LAUNCHER_EXE
9+
from dataclasses import dataclass
10+
from galaxy.proc_tools import process_iter, ProcessInfo
11+
12+
@dataclass
13+
class RunningGame:
14+
name: str
15+
process: ProcessInfo
16+
17+
18+
class LocalClient(object):
19+
def __init__(self):
20+
self._local_client_path = None
21+
self._local_client_exe = None
22+
self._local_games_path = None
23+
24+
25+
@property
26+
def installed(self):
27+
if self._local_client_exe and os.access(self._local_client_exe, os.F_OK):
28+
return True
29+
else:
30+
self.refresh_local_client_state()
31+
return self._local_client_exe and os.access(self._local_client_exe, os.F_OK)
32+
33+
@property
34+
def local_client_exe(self):
35+
if self.installed:
36+
return self._local_client_exe
37+
38+
@property
39+
def local_client_path(self):
40+
if self.installed:
41+
return self._local_client_path
42+
43+
@property
44+
def bootstraper_exe(self):
45+
if self.installed:
46+
paradox_root = self._local_client_path[:-len('\\launcher')]
47+
bootstrapper = os.path.join(paradox_root, 'bootstrapper')
48+
return os.path.join(bootstrapper,'Bootstrapper.exe')
49+
50+
@property
51+
def games_path(self):
52+
if self.installed:
53+
if self._local_games_path:
54+
return self._local_games_path
55+
else:
56+
paradox_root = self._local_client_path[:-len('\\launcher')]
57+
paradox_games = os.path.join(paradox_root, 'games')
58+
if not os.access(paradox_games, os.F_OK):
59+
return None
60+
self._local_games_path = paradox_games
61+
return paradox_games
62+
63+
def refresh_local_client_state(self):
64+
if sys.platform == 'win32':
65+
try:
66+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,REGISTRY_LAUNCHER_PATH, 0, winreg.KEY_READ) as key:
67+
local_client_path = winreg.QueryValueEx(key, "Path")[0]
68+
local_client_exe = os.path.join(local_client_path, PARADOX_LAUNCHER_EXE)
69+
self._local_client_path = local_client_path
70+
self._local_client_exe = local_client_exe
71+
except OSError:
72+
self._local_client_exe = self._local_client_path = self._local_games_path = None
73+
74+
async def get_running_game(self, games, proc_iter_interval=0.05):
75+
if not games:
76+
return
77+
for process_info in process_iter():
78+
try:
79+
await asyncio.sleep(proc_iter_interval)
80+
for game in games:
81+
if process_info.binary_path.lower() == games[game].lower():
82+
log.info(f"Found a running game! {game}")
83+
return RunningGame(name=game, process=process_info)
84+
except:
85+
continue

0 commit comments

Comments
 (0)