Skip to content

Commit 8f32d44

Browse files
rel 0.43
1 parent 00ceb6a commit 8f32d44

File tree

3 files changed

+212
-179
lines changed

3 files changed

+212
-179
lines changed

src/backend.py

Lines changed: 17 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import json
22
import logging
3-
import time
43
from collections import namedtuple
54
from datetime import datetime
65
from typing import Dict, List, NewType, Optional, Any, Tuple
76

8-
import aiohttp
97
from galaxy.api.errors import (
10-
AccessDenied, AuthenticationRequired, BackendError, BackendNotAvailable, BackendTimeout, NetworkError,
118
UnknownBackendResponse
129
)
1310
from galaxy.api.types import Achievement, SubscriptionGame, Subscription
14-
from galaxy.http import HttpClient
15-
from yarl import URL
1611
from datetime import datetime
1712

18-
1913
logger = logging.getLogger(__name__)
2014
logger.setLevel(logging.INFO)
2115

22-
2316
MasterTitleId = NewType("MasterTitleId", str)
2417
AchievementSet = NewType("AchievementSet", str)
2518
OfferId = NewType("OfferId", str)
@@ -29,136 +22,6 @@
2922

3023
SubscriptionDetails = namedtuple('SubscriptionDetails', ['tier', 'end_time'])
3124

32-
33-
class CookieJar(aiohttp.CookieJar):
34-
def __init__(self):
35-
super().__init__()
36-
self._cookies_updated_callback = None
37-
38-
def set_cookies_updated_callback(self, callback):
39-
self._cookies_updated_callback = callback
40-
41-
def update_cookies(self, cookies, url=URL()):
42-
super().update_cookies(cookies, url)
43-
if cookies and self._cookies_updated_callback:
44-
self._cookies_updated_callback(list(self))
45-
46-
47-
class AuthenticatedHttpClient(HttpClient):
48-
def __init__(self):
49-
self._auth_lost_callback = None
50-
self._cookie_jar = CookieJar()
51-
self._access_token = None
52-
self._last_access_token_success = None
53-
self._save_lats_callback = None
54-
super().__init__(cookie_jar=self._cookie_jar)
55-
56-
def set_auth_lost_callback(self, callback):
57-
self._auth_lost_callback = callback
58-
59-
def set_cookies_updated_callback(self, callback):
60-
self._cookie_jar.set_cookies_updated_callback(callback)
61-
62-
async def authenticate(self, cookies):
63-
self._cookie_jar.update_cookies(cookies)
64-
await self._get_access_token()
65-
66-
def is_authenticated(self):
67-
return self._access_token is not None
68-
69-
async def get(self, *args, **kwargs):
70-
if not self._access_token:
71-
raise AccessDenied("No access token")
72-
73-
try:
74-
return await self._authorized_get(*args, **kwargs)
75-
except (AuthenticationRequired, AccessDenied):
76-
await self._refresh_token()
77-
return await self._authorized_get(*args, **kwargs)
78-
79-
async def _authorized_get(self, *args, **kwargs):
80-
headers = kwargs.setdefault("headers", {})
81-
headers["Authorization"] = "Bearer {}".format(self._access_token)
82-
83-
return await super().request("GET", *args, **kwargs)
84-
85-
async def _refresh_token(self):
86-
try:
87-
if self._access_token is not None:
88-
# diff method, once you have one access_token, you can get another one on refresh.
89-
url = "https://accounts.ea.com/connect/auth"
90-
params = {
91-
"client_id": "JUNO_PC_CLIENT",
92-
"scope": "signin dp.client.default",
93-
"access_token": self._access_token,
94-
}
95-
response = await super().request("GET", url, params=params, allow_redirects=False)
96-
if "access_token" in response.headers["Location"]:
97-
data = response.headers["Location"]
98-
# should look like qrc:/html/login_successful.html#access_token=
99-
# note that there's some other parameters afterwards, so we need to isolate the variable well
100-
self._access_token = data.split("#")[1].split("=")[1].split("&")[0]
101-
else:
102-
await self._get_access_token()
103-
except (BackendNotAvailable, BackendTimeout, BackendError, NetworkError):
104-
logger.warning("Failed to refresh token for independent reasons")
105-
raise
106-
except Exception:
107-
logger.exception("Failed to refresh token")
108-
self._access_token = None
109-
if self._auth_lost_callback:
110-
self._auth_lost_callback()
111-
raise AccessDenied("Failed to refresh token")
112-
113-
async def _get_access_token(self):
114-
url = "https://accounts.ea.com/connect/auth"
115-
params = {
116-
"client_id": "JUNO_PC_CLIENT",
117-
"display": "junoWeb/login",
118-
"response_type": "token",
119-
"redirectUri": "nucleus:rest"
120-
}
121-
response = await super().request("GET", url, params=params, allow_redirects=False)
122-
123-
# upd 18.09.2023 : the access_token is in the "Location" header. It's a Bearer token.
124-
if "access_token" in response.headers["Location"]:
125-
data = response.headers["Location"]
126-
# should look like qrc:/html/login_successful.html#access_token=
127-
# note that there's some other parameters afterwards, so we need to isolate the variable well
128-
self._access_token = data.split("#")[1].split("=")[1].split("&")[0]
129-
elif "access_token" not in response.headers["Location"] and "error=login_required" in response.headers["Location"]:
130-
self._log_session_details()
131-
raise AuthenticationRequired("Error parsing access token. Must reauthenticate.")
132-
else:
133-
self._save_lats()
134-
135-
# more logging for auth lost investigation
136-
137-
def _save_lats(self):
138-
if self._save_lats_callback is not None:
139-
self._last_access_token_success = int(time.time())
140-
self._save_lats_callback(self._last_access_token_success)
141-
142-
def set_save_lats_callback(self, callback):
143-
self._save_lats_callback = callback
144-
145-
def load_lats_from_cache(self, value: Optional[str]):
146-
self._last_access_token_success = int(value) if value else None
147-
148-
def _log_session_details(self):
149-
try:
150-
utag_main_cookie = next(filter(lambda c: c.key == 'utag_main', self._cookie_jar))
151-
utag_main = {i.split(':')[0]: i.split(':')[1] for i in utag_main_cookie.value.split('$')}
152-
logger.info('now: %s st: %s ses_id: %s lats: %s',
153-
str(int(time.time())),
154-
utag_main['_st'][:10],
155-
utag_main['ses_id'][:10],
156-
str(self._last_access_token_success)
157-
)
158-
except Exception as e:
159-
logger.warning('Failed to get session duration: %s', repr(e))
160-
161-
16225
class EABackendClient:
16326
def __init__(self, http_client):
16427
self._http_client = http_client
@@ -171,24 +34,22 @@ def _get_api_host():
17134
async def get_identity(self) -> Tuple[str, str, str]:
17235
url = "{}?query=query{{me{{player{{pd psd displayName}}}}}}".format(self._get_api_host())
17336
pid_response = await self._http_client.get(url)
174-
data = await pid_response.json()
17537

17638
try:
177-
user_id = data["data"]["me"]["player"]["pd"]
178-
persona_id = data["data"]["me"]["player"]["psd"]
179-
user_name = data["data"]["me"]["player"]["displayName"]
39+
user_id = pid_response["data"]["me"]["player"]["pd"]
40+
persona_id = pid_response["data"]["me"]["player"]["psd"]
41+
user_name = pid_response["data"]["me"]["player"]["displayName"]
18042

18143
return str(user_id), str(persona_id), str(user_name)
18244
except (AttributeError, KeyError) as e:
183-
logger.exception("Can not parse backend response: %s, error %s", data, repr(e))
45+
logger.exception("Can not parse backend response: %s, error %s", pid_response, repr(e))
18446
raise UnknownBackendResponse()
18547

18648
async def get_entitlements(self) -> List[Json]:
18749
# Step 1 = get all Origin product IDs
18850
u1 = "{}?query=query{{me{{ownedGameProducts(locale:\"en\" entitlementEnabled:true storefronts:[EA,STEAM,EPIC] type:[DIGITAL_FULL_GAME,PACKAGED_FULL_GAME] platforms:[PC] paging:{{limit:9999}}){{items{{originOfferId product{{gameSlug baseItem {{gameType}} gameProductUser{{ownershipMethods entitlementId}}}}}}}}}}}}".format(self._get_api_host())
189-
r1 = await self._http_client.get(u1)
51+
d1 = await self._http_client.get(u1)
19052
try:
191-
d1 = await r1.json()
19253
return d1['data']['me']['ownedGameProducts']['items']
19354
except (ValueError, KeyError) as e:
19455
logger.exception("Can not parse backend response: %s, error %s", await d1.text(), repr(e))
@@ -203,8 +64,7 @@ async def get_offer(self, offer_id) -> Json:
20364
u2 = u2.replace(' ', '%20').replace('+', '%20')
20465
response = await self._http_client.get(u2)
20566
try:
206-
r2 = await response.json()
207-
return r2['data']['legacyOffers'][0], r2['data']['gameProducts']['items'][0]
67+
return response['data']['legacyOffers'][0], response['data']['gameProducts']['items'][0]
20868
except (ValueError, KeyError) as e:
20969
logger.exception("Can not parse backend response: %s, error %s", await response.text(), repr(e))
21070
raise UnknownBackendResponse()
@@ -236,9 +96,8 @@ def parser(json_data: Dict) -> List[Achievement]:
23696
return achievements
23797

23898
try:
239-
json = await response.json()
24099
achievement_sets = {}
241-
for achievement_set in json["data"]["achievements"]:
100+
for achievement_set in response["data"]["achievements"]:
242101
achievements = parser(achievement_set)
243102
achievement_sets[achievement_set["id"]] = achievements
244103
return achievement_sets
@@ -252,8 +111,7 @@ async def get_achievement_set(self, offer_id: OfferId, persona_id: str) -> str:
252111
response = await self._http_client.get(url)
253112

254113
try:
255-
json = await response.json()
256-
achievements = json["data"]["achievements"]
114+
achievements = response["data"]["achievements"]
257115
if achievements:
258116
return achievements[0]["id"] if "id" in achievements[0] else None
259117
else:
@@ -288,6 +146,7 @@ async def get_game_time(self, game_slug):
288146
}
289147
}
290148
"""
149+
291150
try:
292151
def parse_last_played_time(lastplayed_timestamp) -> Optional[int]:
293152
try:
@@ -297,13 +156,12 @@ def parse_last_played_time(lastplayed_timestamp) -> Optional[int]:
297156

298157
return int(time_delta.total_seconds())
299158

300-
content = await response.json()
301159
# assuming this is just EA's way of saying we never played a game.
302-
if not content['data']['me']['recentGames']['items']:
160+
if not response['data']['me']['recentGames']['items']:
303161
return 0, None
304162
else:
305-
total_play_time = round(int(content['data']['me']['recentGames']['items'][0]['totalPlayTimeSeconds']) / 60) # response is in seconds
306-
last_played_time = parse_last_played_time(content['data']['me']['recentGames']['items'][0]['lastSessionEndDate'])
163+
total_play_time = round(int(response['data']['me']['recentGames']['items'][0]['totalPlayTimeSeconds']) / 60) # response is in seconds
164+
last_played_time = parse_last_played_time(response['data']['me']['recentGames']['items'][0]['lastSessionEndDate'])
307165

308166
return total_play_time, last_played_time
309167
except (AttributeError, ValueError, KeyError) as e:
@@ -338,10 +196,9 @@ async def get_friends(self):
338196
"""
339197

340198
try:
341-
content = await response.json()
342199
return {
343200
user_json['player']['pd']: user_json["player"]["displayName"]
344-
for user_json in content["data"]["me"]["friends"]["items"]
201+
for user_json in response["data"]["me"]["friends"]["items"]
345202
}
346203
except (AttributeError, KeyError):
347204
logger.exception("Can not parse backend response: %s", await response.text())
@@ -382,8 +239,7 @@ def parse_last_session_end_date(date) -> int:
382239

383240

384241
try:
385-
content = await response.json()
386-
games = content["data"]["me"]["recentGames"]["items"]
242+
games = response["data"]["me"]["recentGames"]["items"]
387243
return {
388244
game["gameSlug"]: parse_last_session_end_date(game["lastSessionEndDate"])
389245
for game in games
@@ -414,8 +270,7 @@ async def _get_subscription_uris(self) -> List[str]:
414270
url = "{}?query=query{{me{{subscriptions{{offerId recurring start end level status offer{{offerName duration}} platform type statusReasonCode acquisitionMethod}}}}}}".format(self._get_api_host())
415271
response = await self._http_client.get(url)
416272
try:
417-
data = await response.json()
418-
return data['data']['me']['subscriptions']
273+
return response['data']['me']['subscriptions']
419274
except (ValueError, KeyError) as e:
420275
logger.exception("Can not parse backend response while getting subs uri: %s, error %s", await response.text(), repr(e))
421276
raise UnknownBackendResponse()
@@ -451,17 +306,15 @@ async def get_games_in_subscription(self, tier) -> List[SubscriptionGame]:
451306
url = "{}?query=query{{gameSearch(filter:{{gameTypes:[BASE_GAME],productLifecycleFilter:{{lifecycleTypes:[{}]}}}},paging:{{limit:9999}}){{items{{slug}}}}}}".format(self._get_api_host(), tier)
452307
response = await self._http_client.get(url)
453308
try:
454-
slugs = await response.json()
455-
slugs = [game['slug'] for game in slugs['data']['gameSearch']['items']]
309+
slugs = [game['slug'] for game in response['data']['gameSearch']['items']]
456310
# we'll only get slugs, now get entitlement data
457311
subscription_games = [] # Create an empty list to accumulate the subscription games
458312
url2 = "{}?query=query{{games(slugs:{}){{items{{slug products{{items{{id name originOfferId}}}}}}}}}}".format(
459313
self._get_api_host(),
460314
json.dumps(slugs)
461315
)
462-
res2 = await self._http_client.get(url2.replace(' ', '%20').replace('+', '%20'))
316+
games = await self._http_client.get(url2.replace(' ', '%20').replace('+', '%20'))
463317
try:
464-
games = await res2.json()
465318
# verify product info, and take the correct Origin offer ID (some games have multiple offers)
466319
for game in games['data']['games']['items']:
467320
if len(game['products']['items']) == 1:

0 commit comments

Comments
 (0)