Skip to content

Commit ca84ee3

Browse files
authored
Merge pull request #33 from srhinos/api-rewrite
Refactor Code Base to Use GraphQL API over headless Browser Automation
2 parents 43760e7 + 567d2d9 commit ca84ee3

6 files changed

+475
-353
lines changed

Dockerfile

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
FROM mcr.microsoft.com/playwright/python:next
1+
FROM python:3.11
22

33
WORKDIR /app
44

55
COPY requirements.txt requirements.txt
66
RUN pip install -r requirements.txt
7-
RUN python -m playwright install
87

98
COPY primelooter.py primelooter.py
109
CMD [ "python", "primelooter.py" , "--loop" ]

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ services:
99
- ./publishers.txt:/app/publishers.txt # must exist before launching
1010
- ./game_codes.txt:/app/game_codes.txt # must exist before launching
1111
environment:
12-
- TZ=Europe/Berlin
12+
- TZ=America/New_York

experiment.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import httpx
2+
import json
3+
import asyncio
4+
import re
5+
import logging
6+
import http.cookiejar as cookiejar
7+
8+
gql_url = "https://gaming.amazon.com/graphql"
9+
10+
logging.getLogger("httpx").setLevel(logging.WARNING)
11+
log = logging.getLogger()
12+
offers_payload = {
13+
"operationName": "OffersContext_Offers_And_Items",
14+
"variables": {"pageSize": 999},
15+
"extensions": {},
16+
"query": "query OffersContext_Offers_And_Items($dateOverride: Time, $pageSize: Int) {\n inGameLoot: items(\n collectionType: LOOT\n dateOverride: $dateOverride\n pageSize: $pageSize\n ) {\n items {\n ...Item\n __typename\n }\n __typename\n }\n}\n\nfragment Item on Item {\n id\n isDirectEntitlement\n requiresLinkBeforeClaim\n grantsCode\n isDeepLink\n isFGWP\n offers {\n ...Item_Offer\n __typename\n }\n game {\n ...Game\n __typename\n }\n __typename\n}\n\n\nfragment Item_Offer on Offer {\n id\n offerSelfConnection {\n eligibility {\n ...Item_Offer_Eligibility\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment Item_Offer_Eligibility on OfferEligibility {\n isClaimed\n canClaim\n missingRequiredAccountLink\n}\n\nfragment Game on GameV2 {\n id\n assets {\n title\n publisher\n }\n}\n",
17+
}
18+
19+
20+
async def claim_offer(offer_id: str, item: dict, client: httpx.AsyncClient, headers: dict) -> True:
21+
if item["offers"][0]["offerSelfConnection"]["eligibility"]["isClaimed"] != True:
22+
if (
23+
item["offers"][0]["offerSelfConnection"]["eligibility"]["canClaim"] == False
24+
and item["offers"][0]["offerSelfConnection"]["eligibility"]["missingRequiredAccountLink"] == True
25+
):
26+
log.error(f"Cannot collect game `{item['game']['assets']['title']}`, account link required.")
27+
return
28+
log.info(f"Collecting offer for {item['game']['assets']['title']}")
29+
claim_payload = {
30+
"operationName": "placeOrdersDetailPage",
31+
"variables": {
32+
"input": {
33+
"offerIds": [offer_id],
34+
"attributionChannel": '{"eventId":"ItemDetailRootPage:' + offer_id + '","page":"ItemDetailPage"}',
35+
}
36+
},
37+
"extensions": {},
38+
"query": "fragment Place_Orders_Payload_Order_Information on OfferOrderInformation {\n catalogOfferId\n claimCode\n entitledAccountId\n entitledAccountName\n id\n orderDate\n orderState\n __typename\n}\n\nmutation placeOrdersDetailPage($input: PlaceOrdersInput!) {\n placeOrders(input: $input) {\n error {\n code\n __typename\n }\n orderInformation {\n ...Place_Orders_Payload_Order_Information\n __typename\n }\n __typename\n }\n}\n",
39+
}
40+
41+
response = await client.post(gql_url, headers=headers, data=json.dumps(claim_payload))
42+
if response.json()["data"]["placeOrders"]["error"] != None:
43+
log.error(f"Error: {response.json()['data']['placeOrders']['error']}")
44+
45+
46+
async def primelooter(cookie_file):
47+
jar = cookiejar.MozillaCookieJar(cookie_file)
48+
jar.load()
49+
async with httpx.AsyncClient() as client:
50+
base_headers = {
51+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0",
52+
}
53+
54+
json_headers = base_headers | {
55+
"Content-Type": "application/json",
56+
}
57+
for _c in jar:
58+
client.cookies.jar.set_cookie(_c)
59+
60+
html_body = (await client.get("https://gaming.amazon.com/home", headers=base_headers)).text
61+
matches = re.findall(r"name='csrf-key' value='(.*)'", html_body)
62+
json_headers["csrf-token"] = matches[0]
63+
64+
response = await client.post(gql_url, headers=json_headers, data=json.dumps(offers_payload))
65+
data = response.json()["data"]["inGameLoot"]["items"]
66+
67+
coros = await asyncio.gather(
68+
*[claim_offer(item["offers"][0]["id"], item, client, json_headers) for item in data]
69+
)

0 commit comments

Comments
 (0)