mirror of
https://github.com/frozenpandaman/s3s.git
synced 2026-07-22 02:10:33 -05:00
major refactor to address #62 & improve structure
This commit is contained in:
parent
a4425bf461
commit
487ab596da
148
iksm.py
148
iksm.py
|
|
@ -6,35 +6,112 @@ import base64, hashlib, json, os, re, sys
|
|||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
session = requests.Session()
|
||||
S3S_VERSION = "unknown"
|
||||
NSOAPP_VERSION = "2.3.1"
|
||||
S3S_VERSION = "unknown"
|
||||
NSOAPP_VERSION = "unknown"
|
||||
NSOAPP_VER_FALLBACK = "2.3.1"
|
||||
WEB_VIEW_VERSION = "unknown"
|
||||
WEB_VIEW_VER_FALLBACK = "1.0.0-5644e7a2" # fallback for current splatnet 3 ver
|
||||
SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net"
|
||||
|
||||
# functions in this file & call stack:
|
||||
# get_nsoapp_version()
|
||||
# log_in() -> get_session_token()
|
||||
# get_gtoken() -> call_imink_api() -> f
|
||||
# get_bullet()
|
||||
# enter_tokens()
|
||||
# - get_nsoapp_version()
|
||||
# - get_web_view_ver()
|
||||
# - log_in() -> get_session_token()
|
||||
# - get_gtoken() -> call_f_api()
|
||||
# - get_bullet()
|
||||
# - enter_tokens()
|
||||
|
||||
# place config.txt in same directory as script (bundled or not)
|
||||
if getattr(sys, 'frozen', False):
|
||||
app_path = os.path.dirname(sys.executable)
|
||||
elif __file__:
|
||||
app_path = os.path.dirname(__file__)
|
||||
config_path = os.path.join(app_path, "config.txt")
|
||||
session = requests.Session()
|
||||
|
||||
def get_nsoapp_version():
|
||||
'''Fetches the current Nintendo Switch Online app version from the Apple App Store.'''
|
||||
'''Fetches the current Nintendo Switch Online app version from the Apple App Store and sets it globally.'''
|
||||
|
||||
try:
|
||||
page = requests.get("https://apps.apple.com/us/app/nintendo-switch-online/id1234806557")
|
||||
soup = BeautifulSoup(page.text, 'html.parser')
|
||||
elt = soup.find("p", {"class": "whats-new__latest__version"})
|
||||
ver = elt.get_text().replace("Version ", "").strip()
|
||||
return ver
|
||||
except:
|
||||
global NSOAPP_VERSION
|
||||
if NSOAPP_VERSION != "unknown": # already set
|
||||
return NSOAPP_VERSION
|
||||
else:
|
||||
try:
|
||||
page = requests.get("https://apps.apple.com/us/app/nintendo-switch-online/id1234806557")
|
||||
soup = BeautifulSoup(page.text, 'html.parser')
|
||||
elt = soup.find("p", {"class": "whats-new__latest__version"})
|
||||
ver = elt.get_text().replace("Version ", "").strip()
|
||||
|
||||
NSOAPP_VERSION = ver
|
||||
|
||||
return NSOAPP_VERSION
|
||||
except: # error with web request
|
||||
return NSOAPP_VER_FALLBACK
|
||||
|
||||
|
||||
def get_web_view_ver(bhead=[], gtoken=""):
|
||||
'''Finds & parses the SplatNet 3 main.js file to fetch the current site version and sets it globally.'''
|
||||
|
||||
global WEB_VIEW_VERSION
|
||||
if WEB_VIEW_VERSION != "unknown":
|
||||
return WEB_VIEW_VERSION
|
||||
else:
|
||||
app_head = {
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Accept': '*/*',
|
||||
'DNT': '1',
|
||||
'X-AppColorScheme': 'DARK',
|
||||
'X-Requested-With': 'com.nintendo.znca',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Sec-Fetch-Dest': 'document'
|
||||
}
|
||||
app_cookies = {
|
||||
'_dnt': '1' # Do Not Track
|
||||
}
|
||||
|
||||
if bhead:
|
||||
app_head["User-Agent"] = bhead.get("User-Agent")
|
||||
app_head["Accept-Encoding"] = bhead.get("Accept-Encoding")
|
||||
app_head["Accept-Language"] = bhead.get("Accept-Language")
|
||||
if gtoken:
|
||||
app_cookies["_gtoken"] = gtoken # X-GameWebToken
|
||||
|
||||
home = requests.get(SPLATNET3_URL, headers=app_head, cookies=app_cookies)
|
||||
if home.status_code != 200:
|
||||
return WEB_VIEW_VER_FALLBACK
|
||||
|
||||
soup = BeautifulSoup(home.text, "html.parser")
|
||||
main_js = soup.select_one("script[src*='static']")
|
||||
|
||||
if not main_js: # failed to parse html for main.js file
|
||||
return WEB_VIEW_VER_FALLBACK
|
||||
|
||||
main_js_url = SPLATNET3_URL + main_js.attrs["src"]
|
||||
|
||||
app_head = {
|
||||
'Accept': '*/*',
|
||||
'X-Requested-With': 'com.nintendo.znca',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Dest': 'script'
|
||||
}
|
||||
if bhead:
|
||||
app_head["User-Agent"] = bhead.get("User-Agent")
|
||||
app_head["Referer"] = bhead.get("Referer")
|
||||
app_head["Accept-Encoding"] = bhead.get("Accept-Encoding")
|
||||
app_head["Accept-Language"] = bhead.get("Accept-Language")
|
||||
|
||||
main_js_body = requests.get(main_js_url, headers=app_head, cookies=app_cookies)
|
||||
if main_js_body.status_code != 200:
|
||||
return iksm.WEB_VIEW_VER_FALLBACK
|
||||
|
||||
pattern = r"\b(?P<revision>[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P<version>\d+\.\d+\.\d+)"
|
||||
match = re.search(pattern, main_js_body.text)
|
||||
if match is None:
|
||||
return iksm.WEB_VIEW_VER_FALLBACK
|
||||
|
||||
version, revision = match.group("version"), match.group("revision")
|
||||
ver_string = f"{version}-{revision[:8]}"
|
||||
|
||||
WEB_VIEW_VERSION = ver_string
|
||||
|
||||
return WEB_VIEW_VERSION
|
||||
|
||||
|
||||
def log_in(ver, app_user_agent):
|
||||
|
|
@ -128,7 +205,7 @@ def get_session_token(session_token_code, auth_code_verifier):
|
|||
|
||||
|
||||
def get_gtoken(f_gen_url, session_token, ver):
|
||||
'''Provided the session_token, returns a GameWebToken and account info.'''
|
||||
'''Provided the session_token, returns a GameWebToken JWT and account info.'''
|
||||
|
||||
nsoapp_version = get_nsoapp_version()
|
||||
|
||||
|
|
@ -184,7 +261,7 @@ def get_gtoken(f_gen_url, session_token, ver):
|
|||
body = {}
|
||||
try:
|
||||
id_token = id_response["id_token"]
|
||||
f, uuid, timestamp = call_imink_api(id_token, 1, f_gen_url)
|
||||
f, uuid, timestamp = call_f_api(id_token, 1, f_gen_url)
|
||||
|
||||
parameter = {
|
||||
'f': f,
|
||||
|
|
@ -223,7 +300,7 @@ def get_gtoken(f_gen_url, session_token, ver):
|
|||
except:
|
||||
# retry once if 9403/9599 error from nintendo
|
||||
try:
|
||||
f, uuid, timestamp = call_imink_api(id_token, 1, f_gen_url)
|
||||
f, uuid, timestamp = call_f_api(id_token, 1, f_gen_url)
|
||||
body["parameter"]["f"] = f
|
||||
body["parameter"]["requestId"] = uuid
|
||||
body["parameter"]["timestamp"] = timestamp
|
||||
|
|
@ -238,7 +315,7 @@ def get_gtoken(f_gen_url, session_token, ver):
|
|||
print("Re-running the script usually fixes this.")
|
||||
sys.exit(1)
|
||||
|
||||
f, uuid, timestamp = call_imink_api(id_token, 2, f_gen_url)
|
||||
f, uuid, timestamp = call_f_api(id_token, 2, f_gen_url)
|
||||
|
||||
# get web service token
|
||||
app_head = {
|
||||
|
|
@ -270,7 +347,7 @@ def get_gtoken(f_gen_url, session_token, ver):
|
|||
except:
|
||||
# retry once if 9403/9599 error from nintendo
|
||||
try:
|
||||
f, uuid, timestamp = call_imink_api(id_token, 2, f_gen_url)
|
||||
f, uuid, timestamp = call_f_api(id_token, 2, f_gen_url)
|
||||
body["parameter"]["f"] = f
|
||||
body["parameter"]["requestId"] = uuid
|
||||
body["parameter"]["timestamp"] = timestamp
|
||||
|
|
@ -286,25 +363,25 @@ def get_gtoken(f_gen_url, session_token, ver):
|
|||
return web_service_token, user_nickname, user_lang, user_country
|
||||
|
||||
|
||||
def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_country):
|
||||
'''Returns a bulletToken.'''
|
||||
def get_bullet(web_service_token, app_user_agent, user_lang, user_country):
|
||||
'''Given a gtoken, returns a bulletToken.'''
|
||||
|
||||
app_head = {
|
||||
'Content-Length': '0',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': user_lang,
|
||||
'User-Agent': app_user_agent,
|
||||
'X-Web-View-Ver': web_view_ver,
|
||||
'X-Web-View-Ver': get_web_view_ver(),
|
||||
'X-NACOUNTRY': user_country,
|
||||
'Accept': '*/*',
|
||||
'Origin': 'https://api.lp1.av5ja.srv.nintendo.net',
|
||||
'Origin': SPLATNET3_URL,
|
||||
'X-Requested-With': 'com.nintendo.znca'
|
||||
}
|
||||
app_cookies = {
|
||||
'_gtoken': web_service_token, # X-GameWebToken
|
||||
'_dnt': '1' # Do Not Track
|
||||
}
|
||||
url = "https://api.lp1.av5ja.srv.nintendo.net/api/bullet_tokens"
|
||||
url = f'{SPLATNET3_URL}/api/bullet_tokens'
|
||||
r = requests.post(url, headers=app_head, cookies=app_cookies)
|
||||
|
||||
if r.status_code == 401:
|
||||
|
|
@ -332,8 +409,8 @@ def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_
|
|||
return bullet_token
|
||||
|
||||
|
||||
def call_imink_api(id_token, step, f_gen_url):
|
||||
'''Passes in an naIdToken to the imink API and fetches the response (comprised of an f token, UUID, and timestamp).'''
|
||||
def call_f_api(id_token, step, f_gen_url):
|
||||
'''Passes an naIdToken to the f generation API (default: imink) & fetches the response (f token, UUID, and timestamp).'''
|
||||
|
||||
try:
|
||||
api_head = {
|
||||
|
|
@ -366,7 +443,8 @@ def call_imink_api(id_token, step, f_gen_url):
|
|||
def enter_tokens():
|
||||
'''Prompts the user to enter a gtoken and bulletToken.'''
|
||||
|
||||
print("Go to the page below to find instructions to obtain your gtoken and bulletToken:\nhttps://github.com/frozenpandaman/s3s/wiki/mitmproxy-instructions\n")
|
||||
print("Go to the page below to find instructions to obtain your gtoken and bulletToken:")
|
||||
print("https://github.com/frozenpandaman/s3s/wiki/mitmproxy-instructions\n")
|
||||
|
||||
new_gtoken = input("Enter your gtoken: ")
|
||||
while len(new_gtoken) != 926:
|
||||
|
|
|
|||
26
s3s.py
26
s3s.py
|
|
@ -8,8 +8,7 @@ import argparse, datetime, json, os, shutil, re, requests, sys, time, uuid
|
|||
import msgpack
|
||||
import iksm, utils
|
||||
|
||||
A_VERSION = "0.1.9"
|
||||
WEB_VIEW_VERSION = "unknown" # set in prefetch_checks()
|
||||
A_VERSION = "0.1.10"
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
|
@ -39,10 +38,6 @@ except (IOError, ValueError):
|
|||
CONFIG_DATA = json.load(config_file)
|
||||
config_file.close()
|
||||
|
||||
DEFAULT_USER_AGENT = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) ' \
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
|
||||
'Chrome/94.0.4606.61 Mobile Safari/537.36'
|
||||
|
||||
# SET GLOBALS
|
||||
API_KEY = CONFIG_DATA["api_key"] # for stat.ink
|
||||
USER_LANG = CONFIG_DATA["acc_loc"][:5] # nintendo account info
|
||||
|
|
@ -53,6 +48,9 @@ SESSION_TOKEN = CONFIG_DATA["session_token"] # for nintendo login
|
|||
F_GEN_URL = CONFIG_DATA["f_gen"] # endpoint for generating f (imink API by default)
|
||||
|
||||
# SET HTTP HEADERS
|
||||
DEFAULT_USER_AGENT = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) ' \
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
|
||||
'Chrome/94.0.4606.61 Mobile Safari/537.36'
|
||||
APP_USER_AGENT = str(CONFIG_DATA.get("app_user_agent", DEFAULT_USER_AGENT))
|
||||
|
||||
|
||||
|
|
@ -90,12 +88,12 @@ def headbutt():
|
|||
'Authorization': f'Bearer {BULLETTOKEN}', # update every time it's called with current global var
|
||||
'Accept-Language': USER_LANG,
|
||||
'User-Agent': APP_USER_AGENT,
|
||||
'X-Web-View-Ver': WEB_VIEW_VERSION,
|
||||
'X-Web-View-Ver': iksm.WEB_VIEW_VERSION,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': '*/*',
|
||||
'Origin': 'https://api.lp1.av5ja.srv.nintendo.net',
|
||||
'Origin': iksm.SPLATNET3_URL,
|
||||
'X-Requested-With': 'com.nintendo.znca',
|
||||
'Referer': f'https://api.lp1.av5ja.srv.nintendo.net/?lang={USER_LANG}&na_country={USER_COUNTRY}&na_lang={USER_LANG}',
|
||||
'Referer': f'{iksm.SPLATNET3_URL}?lang={USER_LANG}&na_country={USER_COUNTRY}&na_lang={USER_LANG}',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
}
|
||||
return graphql_head
|
||||
|
|
@ -107,12 +105,12 @@ def prefetch_checks(printout=False):
|
|||
if printout:
|
||||
print("Validating your tokens...", end='\r')
|
||||
|
||||
global WEB_VIEW_VERSION
|
||||
WEB_VIEW_VERSION = iksm.get_web_view_ver()
|
||||
|
||||
if SESSION_TOKEN == "" or GTOKEN == "" or BULLETTOKEN == "":
|
||||
gen_new_tokens("blank")
|
||||
|
||||
global WEB_VIEW_VERSION
|
||||
WEB_VIEW_VERSION = utils.get_web_view_ver(headbutt(), GTOKEN)
|
||||
|
||||
sha = utils.translate_rid["HomeQuery"]
|
||||
test = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), headers=headbutt(), cookies=dict(_gtoken=GTOKEN))
|
||||
if test.status_code != 200:
|
||||
|
|
@ -130,7 +128,7 @@ def gen_new_tokens(reason, force=False):
|
|||
manual_entry = False
|
||||
if force != True: # unless we force our way through
|
||||
if reason == "blank":
|
||||
print("Blank token(s).")
|
||||
print("Blank token(s). ")
|
||||
elif reason == "expiry":
|
||||
print("The stored tokens have expired.")
|
||||
else:
|
||||
|
|
@ -160,7 +158,7 @@ def gen_new_tokens(reason, force=False):
|
|||
else:
|
||||
print("Attempting to generate new gtoken and bulletToken...")
|
||||
new_gtoken, acc_name, acc_lang, acc_country = iksm.get_gtoken(F_GEN_URL, SESSION_TOKEN, A_VERSION)
|
||||
new_bullettoken = iksm.get_bullet(new_gtoken, WEB_VIEW_VERSION, APP_USER_AGENT, acc_lang, acc_country)
|
||||
new_bullettoken = iksm.get_bullet(new_gtoken, APP_USER_AGENT, acc_lang, acc_country)
|
||||
CONFIG_DATA["gtoken"] = new_gtoken # valid for 2 hours
|
||||
CONFIG_DATA["bullettoken"] = new_bullettoken # valid for 2 hours
|
||||
CONFIG_DATA["acc_loc"] = acc_lang + "|" + acc_country
|
||||
|
|
|
|||
67
utils.py
67
utils.py
|
|
@ -5,10 +5,10 @@
|
|||
import base64, datetime, json, re, sys, uuid
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import iksm
|
||||
|
||||
SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net"
|
||||
GRAPHQL_URL = "https://api.lp1.av5ja.srv.nintendo.net/api/graphql"
|
||||
FALLBACK_WEB_VIEW_VERSION = "1.0.0-5644e7a2" # fallback for current splatnet 3 ver
|
||||
SPLATNET3_URL = iksm.SPLATNET3_URL
|
||||
GRAPHQL_URL = f'{SPLATNET3_URL}/api/graphql'
|
||||
S3S_NAMESPACE = uuid.UUID('b3a2dbf5-2c09-4792-b78c-00b548b70aeb')
|
||||
|
||||
SUPPORTED_KEYS = [
|
||||
|
|
@ -18,7 +18,7 @@ SUPPORTED_KEYS = [
|
|||
]
|
||||
|
||||
# SHA256 hash database for SplatNet 3 GraphQL queries
|
||||
# full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3737698
|
||||
# full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3614603
|
||||
translate_rid = {
|
||||
'HomeQuery': 'dba47124d5ec3090c97ba17db5d2f4b3', # blank vars
|
||||
'LatestBattleHistoriesQuery': '7d8b560e31617e981cf7c8aa1ca13a00', # INK / blank vars - query1
|
||||
|
|
@ -30,63 +30,6 @@ translate_rid = {
|
|||
'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e' # SR / req "coopHistoryDetailId" - query2
|
||||
}
|
||||
|
||||
def get_web_view_ver(bhead, gtoken):
|
||||
'''Find & parse the SplatNet 3 main.js file for the current site version.'''
|
||||
|
||||
app_head = { # bhead should have all fields from headbutt() & gtoken will be valid
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'User-Agent': bhead["User-Agent"],
|
||||
'Accept': '*/*',
|
||||
'DNT': '1',
|
||||
'X-AppColorScheme': 'DARK',
|
||||
'X-Requested-With': 'com.nintendo.znca',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Accept-Encoding': bhead["Accept-Encoding"],
|
||||
'Accept-Language': bhead["Accept-Language"]
|
||||
}
|
||||
app_cookies = {
|
||||
'_gtoken': gtoken, # X-GameWebToken
|
||||
'_dnt': '1' # Do Not Track
|
||||
}
|
||||
home = requests.get(SPLATNET3_URL, headers=app_head, cookies=app_cookies)
|
||||
if home.status_code != 200:
|
||||
return FALLBACK_WEB_VIEW_VERSION
|
||||
|
||||
soup = BeautifulSoup(home.text, "html.parser")
|
||||
main_js = soup.select_one("script[src*='static']")
|
||||
|
||||
if not main_js: # failed to parse html for main.js file
|
||||
return FALLBACK_WEB_VIEW_VERSION
|
||||
|
||||
main_js_url = SPLATNET3_URL + main_js.attrs["src"]
|
||||
|
||||
app_head = {
|
||||
'User-Agent': bhead["User-Agent"],
|
||||
'Accept': '*/*',
|
||||
'X-Requested-With': 'com.nintendo.znca',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Dest': 'script',
|
||||
'Referer': bhead["Referer"],
|
||||
'Accept-Encoding': bhead["Accept-Encoding"],
|
||||
'Accept-Language': bhead["Accept-Language"]
|
||||
}
|
||||
|
||||
main_js_body = requests.get(main_js_url, headers=app_head, cookies=app_cookies)
|
||||
if main_js_body.status_code != 200:
|
||||
return FALLBACK_WEB_VIEW_VERSION
|
||||
|
||||
pattern = r"\b(?P<revision>[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P<version>\d+\.\d+\.\d+)"
|
||||
match = re.search(pattern, main_js_body.text)
|
||||
if match is None:
|
||||
return FALLBACK_WEB_VIEW_VERSION
|
||||
|
||||
version, revision = match.group("version"), match.group("revision")
|
||||
return f"{version}-{revision[:8]}"
|
||||
|
||||
|
||||
def set_noun(which):
|
||||
'''Returns the term to be used when referring to the type of results in question.'''
|
||||
|
|
@ -100,7 +43,7 @@ def set_noun(which):
|
|||
|
||||
|
||||
def b64d(string):
|
||||
'''Base64 decode a string and cut off the SplatNet prefix.'''
|
||||
'''Base64 decodes a string and cut off the SplatNet prefix.'''
|
||||
|
||||
thing_id = base64.b64decode(string).decode('utf-8')
|
||||
thing_id = thing_id.replace("VsStage-", "")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user