Skip to content

Commit da218b0

Browse files
committed
Update the script to use the REST API instead of Selenium
1 parent 568ff4d commit da218b0

File tree

3 files changed

+80
-93
lines changed

3 files changed

+80
-93
lines changed

README.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,8 @@
33
This python script allows to redeem gift codes for Whiteout Survival in an
44
automated way for a number of players (e.g., all players in an alliance).
55

6-
It utilizes the web page from CenturyGames that is typically used by iOS users
7-
because they cannot redeem gift codes in the app itself. While it was difficult
8-
to extract the exact API calls the page is doing I opted for automating it using
9-
the Selenium framework. Selenium is typically used for testing because it allows
10-
to mimic user interactions with a web site. This creates an obvious downside:
11-
The script only is required to run in attended mode on a personal computer and
12-
currently cannot run on a headless server.
13-
14-
The way it has been implemented works perfectly for my own use case but is far from
15-
perfect. It would ultimately be best if this could run entireliy without Selenium and
16-
call the APIs directly. I suppose that Century Games won't be of much help here so I'll
17-
stick with the current implementation for the time being.
6+
It utilizes the REST API that the web page uses that iOS require to redeem their
7+
codes as the app doesn't have an option for it.
188

199
## Usage
2010

@@ -24,6 +14,29 @@ A sample is provided to understand the structure.
2414

2515
`python redeem_code.py -c <gift-code>`
2616

17+
## Notes on the redemption endpoint
18+
19+
The REST API implementation is not exactly straight-forward because it appears the devs try to
20+
hide it so that it is not used for automation scripts like this one is.
21+
22+
Every request is signed with a key that is calculated in a special way. Looking at the JavaScript
23+
code of the page only shows some obfuscated logic. What essentially happens is: The data that is part
24+
of the request is converted into a URL request string, a salt appended and then hashed using MD5.
25+
26+
A request payload may look like this:
27+
```json
28+
{
29+
"fid": 12345678,
30+
"time": 1716126948359,
31+
"sign": ???
32+
}
33+
```
34+
35+
Now, the sign is calculated using the following logic (pseudo-code) and appended to the payload:
36+
```
37+
("fid=12345678&time=1716126948359" + "tB87#kPtkxqOS2").md5()
38+
```
39+
2740
## Contributions
2841

2942
I don't expect many contributions but if you have suggestions on how to improve this, feel free

redeem_code.py

Lines changed: 55 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,19 @@
11
"""
2-
This script is a Selenium bot that redeems a gift code
3-
for the mobile game Whiteout Survival by using their website
2+
This script redeems a gift code for players of the mobile game
3+
Whiteout Survival by using their API
44
55
It requires an input file that contains all player IDs and
66
tracks its progress in an output file to be able to continue
77
in case it runs into errors without retrying to redeem a code
88
for everyone
9-
10-
This script currently only supports Mac OS because it is based
11-
on the Safari browser
129
"""
1310
import argparse
11+
import hashlib
1412
import json
1513
import sys
14+
import time
1615

17-
from selenium.webdriver.support import ui
18-
from selenium import webdriver
19-
from selenium.common.exceptions import TimeoutException
20-
from selenium.webdriver.common.by import By
21-
22-
23-
def save_results(filename, results_to_save):
24-
"""
25-
This function was needed to make sure we can save progress on TimeoutExceptions
26-
"""
27-
with open(filename, 'w', encoding="utf-8") as fp:
28-
json.dump(results_to_save, fp)
29-
16+
import requests
3017

3118
# Handle arguments the script is called with
3219
parser = argparse.ArgumentParser()
@@ -35,7 +22,7 @@ def save_results(filename, results_to_save):
3522
dest='player_file', default='player.json')
3623
parser.add_argument('-r', '--results-file',
3724
dest='results_file', default='results.json')
38-
parser.add_argument('--restart', type=bool, dest='restart', default=False)
25+
parser.add_argument('--restart', dest='restart', action="store_true")
3926
args = parser.parse_args()
4027

4128
# Open and read the user files
@@ -58,101 +45,89 @@ def save_results(filename, results_to_save):
5845
else:
5946
result = found_item
6047

61-
# Setup Selenium
62-
URL = "https://wos-giftcode.centurygame.com"
63-
driver = webdriver.Safari()
64-
driver.get(URL)
65-
wait = ui.WebDriverWait(driver, 30)
66-
6748
# Some variables that are used to tracking progress
6849
session_counter = 1
6950
counter_successfully_claimed = 0
7051
counter_already_claimed = 0
7152
counter_error = 0
7253

54+
URL = "https://wos-giftcode-api.centurygame.com/api"
55+
# The salt is appended to the string that is then signed using md5 and sent as part of the request
56+
SALT = "tB87#kPtkxqOS2"
57+
HTTP_HEADER = {"Content-Type": "application/x-www-form-urlencoded",
58+
"Accept": "application/json"}
59+
60+
i = 0
7361
for player in players:
7462

63+
# Print progress bar
64+
i += 1
65+
66+
print("\x1b[K" + str(i) + "/" + str(len(players)) +
67+
" complete. Redeeming for " + player["original_name"], end="\r", flush=True)
68+
7569
# Check if the code has been redeemed for this player already
7670
# Continue to the next iteration if it has been
77-
if result["status"].get(player["id"]) == "Successful":
71+
if result["status"].get(player["id"]) == "Successful" and not args.restart:
72+
counter_already_claimed += 1
7873
continue
7974

8075
# This is necessary because we reload the page every 5 players
8176
# and the website isn't sometimes ready before we continue
82-
try:
83-
wait.until(lambda driver: driver.find_element(
84-
By.XPATH, "//input[contains(@placeholder,'Player ID')]"))
85-
except TimeoutException as e:
86-
print("Timeout Exception")
87-
save_results(args.results_file, results)
77+
request_data = {"fid": player["id"], "time": time.time_ns()}
78+
request_data["sign"] = hashlib.md5(("fid=" + request_data["fid"] + "&time=" + str(
79+
request_data["time"]) + SALT).encode("utf-8")).hexdigest()
80+
81+
# Login the player
82+
# It is enough to send the POST request, we don't need to store any cookies/session tokens
83+
# to authenticate during the next request
84+
login_request = requests.post(
85+
URL + '/player', data=request_data, headers=HTTP_HEADER, timeout=30)
86+
login_response = login_request.json()
87+
if login_response["msg"] != "success":
88+
print("Login not possible")
8889
sys.exit(1)
8990

90-
# Enter the player ID
91-
player_id_input = driver.find_element(
92-
By.XPATH, "//input[contains(@placeholder,'Player ID')]")
93-
player_id_input.clear()
94-
player_id_input.send_keys(player["id"])
95-
96-
# Login the player by using the login button
97-
# We are using the Selenium wait feature to make sure the player is logged in before we continue
98-
# We know the player is logged in, once the exit icon appears
99-
login_button = driver.find_element(By.CLASS_NAME, "login_btn").click()
100-
try:
101-
wait.until(lambda driver: driver.find_element(
102-
By.CLASS_NAME, "exit_icon"))
103-
except TimeoutException as e:
104-
print("Timeout Exception")
105-
save_results(args.results_file, results)
106-
sys.exit(1)
91+
# Create the request data that contains the signature and the code
92+
request_data["cdk"] = args.code
93+
request_data["sign"] = hashlib.md5(("cdk=" + request_data["cdk"] + \
94+
"&fid=" + request_data["fid"] + \
95+
"&time=" + str(request_data["time"]) + \
96+
SALT).encode("utf-8")).hexdigest()
10797

108-
# Now we record the login name for later
109-
player["name"] = driver.find_element(By.CLASS_NAME, "name").text
110-
111-
# Enter the gift code and hit confirm
112-
# We again wait until the request is sent before we continue
113-
gift_code_input = driver.find_element(
114-
By.XPATH, "//input[contains(@placeholder,'Enter Gift Code')]")
115-
gift_code_input.clear()
116-
gift_code_input.send_keys(args.code)
117-
redeem_button = driver.find_element(By.CLASS_NAME, "exchange_btn").click()
118-
try:
119-
wait.until(lambda driver: driver.find_element(
120-
By.CLASS_NAME, "confirm_btn"))
121-
except TimeoutException as e:
122-
print("Timeout Exception")
123-
save_results(args.results_file, results)
124-
sys.exit(1)
125-
player["status"] = driver.find_element(By.CLASS_NAME, "msg").text
98+
# Send the gif code redemption request
99+
redeem_request = requests.post(
100+
URL + '/gift_code', data=request_data, headers=HTTP_HEADER, timeout=30)
101+
redeem_response = redeem_request.json()
126102

127103
# In case the gift code is broken, exit straight away
128-
if player["status"] == "Gift Code not found!":
129-
print("The gift code doesn't exist!")
104+
if redeem_response["err_code"] == 40014:
105+
print("\nThe gift code doesn't exist!")
130106
sys.exit(1)
131-
elif player["status"] == "Expired, unable to claim.":
132-
print("The gift code is expired!")
107+
elif redeem_response["err_code"] == 40007:
108+
print("\nThe gift code is expired!")
133109
sys.exit(1)
134-
elif player["status"] == "Already claimed, unable to claim again.":
110+
elif redeem_response["err_code"] == 40008: # ALREADY CLAIMED
135111
counter_already_claimed += 1
136112
result["status"][player["id"]] = "Successful"
137-
elif player["status"] == "Redeemed, please claim the rewards in your mail!":
113+
elif redeem_response["err_code"] == 20000: # SUCCESSFULLY CLAIMED
138114
counter_successfully_claimed += 1
139115
result["status"][player["id"]] = "Successful"
116+
elif redeem_response["err_code"] == 40004: # TIMEOUT RETRY
117+
result["status"][player["id"]] = "Unsuccessful"
140118
else:
141119
result["status"][player["id"]] = "Unsuccessful"
120+
print("\nError occurred: " + str(redeem_response))
142121
counter_error += 1
143122

144-
driver.find_element(By.CLASS_NAME, "confirm_btn").click()
145-
146-
# Now we log the user out again before we continue with the next one
147-
driver.find_element(By.CLASS_NAME, "exit_icon").click()
148-
149123
# Refresh the webpage every 5 players to avoid getting soft-banned at some point
150124
if session_counter % 5 == 0:
151-
driver.refresh()
125+
time.sleep(5)
152126

153127
session_counter += 1
154128

155-
save_results(args.results_file, results)
129+
with open(args.results_file, 'w', encoding="utf-8") as fp:
130+
json.dump(results, fp)
156131

157132
# Print general stats
158133
print("\nSuccessfully claimed gift code for " + str(counter_successfully_claimed) + " players.\n" +

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
selenium==4.20.0

0 commit comments

Comments
 (0)