From a7d8feea327a3ac59a29529e6cbb426f3451f1b1 Mon Sep 17 00:00:00 2001 From: Austin <37423484+Toufool@users.noreply.github.com> Date: Fri, 21 Oct 2022 00:04:50 -0400 Subject: [PATCH 01/20] add S+ number when rank does not change --- s3s.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/s3s.py b/s3s.py index 22a4224..ec7527c 100755 --- a/s3s.py +++ b/s3s.py @@ -588,6 +588,8 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if parent["bankaraMatchChallenge"]["udemaeAfter"] is None: payload["rank_after"] = payload["rank_before"] + if payload["rank_before_s_plus"]: + payload["rank_after_s_plus"] = payload["rank_before_s_plus"] else: if idx != 0: payload["rank_after"] = payload["rank_before"] From 85cf3f79c255c8633d9a509f3b44e8652cbe6c1c Mon Sep 17 00:00:00 2001 From: eli fessler Date: Thu, 20 Oct 2022 18:16:52 -1000 Subject: [PATCH 02/20] code touchups & fix #53 --- s3s.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/s3s.py b/s3s.py index ec7527c..fb5f3fd 100755 --- a/s3s.py +++ b/s3s.py @@ -8,7 +8,7 @@ import argparse, datetime, json, os, shutil, re, requests, sys, time, uuid import msgpack import iksm, utils -A_VERSION = "0.1.6" +A_VERSION = "0.1.7" DEBUG = False @@ -534,13 +534,28 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): payload["our_team_inked"] = our_team_inked payload["their_team_inked"] = their_team_inked - # Anarchy Battles only + if mode == "PRIVATE": # these don't get sent otherwise + try: # could be anarchy, maybe not + payload["knockout"] = "no" if battle["knockout"] is None or battle["knockout"] == "NEITHER" else "yes" + except: + pass + try: + payload["our_team_count"] = battle["myTeam"]["result"]["score"] + payload["their_team_count"] = battle["otherTeams"][0]["result"]["score"] + except: + pass + try: + payload["our_team_percent"] = float(battle["myTeam"]["result"]["paintRatio"]) * 100 + payload["their_team_percent"] = float(battle["otherTeams"][0]["result"]["paintRatio"]) * 100 + except: + pass + if mode == "BANKARA": try: payload["our_team_count"] = battle["myTeam"]["result"]["score"] payload["their_team_count"] = battle["otherTeams"][0]["result"]["score"] - except TypeError: # draw - 'result' is null + except: # draw - 'result' is null pass payload["knockout"] = "no" if battle["knockout"] is None or battle["knockout"] == "NEITHER" else "yes" @@ -1006,6 +1021,8 @@ def monitor_battles(which, secs, isblackout, istestrun): except KeyboardInterrupt: # print(f"\nChecking to see if there are unuploaded {utils.set_noun(which)} before exiting...") # TODO + # check LatestBattleHistoriesQuery against https://stat.ink/api/v3/s3s/uuid-list + # TODO - do update_salmon_profile() at end if salmon run print("\n\nChecking for unuploaded results before exiting is not yet implemented.") print("Please run s3s again with " + '\033[91m' + "-r" + '\033[0m' + " to get these battles.") From b9db1f41da8f33eae69ad03dc76d4885781f847b Mon Sep 17 00:00:00 2001 From: Austin <37423484+Toufool@users.noreply.github.com> Date: Sat, 22 Oct 2022 15:17:57 -0400 Subject: [PATCH 03/20] fix setting rank when not S+ --- s3s.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/s3s.py b/s3s.py index fb5f3fd..3137765 100755 --- a/s3s.py +++ b/s3s.py @@ -586,8 +586,12 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): full_rank = re.split('([0-9]+)', child["udemae"].lower()) payload["rank_before"] = full_rank[0] + if len(full_rank) > 1: + s_plus = True payload["rank_before_s_plus"] = int(full_rank[1]) + else: + s_plus = False # anarchy battle (series) - not open if "bankaraMatchChallenge" in parent and parent["bankaraMatchChallenge"] is not None: @@ -603,7 +607,7 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if parent["bankaraMatchChallenge"]["udemaeAfter"] is None: payload["rank_after"] = payload["rank_before"] - if payload["rank_before_s_plus"]: + if s_plus: payload["rank_after_s_plus"] = payload["rank_before_s_plus"] else: if idx != 0: From 9025fdeefb14a2f2865deecb2ba3b663e338f5ca Mon Sep 17 00:00:00 2001 From: Austin <37423484+Toufool@users.noreply.github.com> Date: Sat, 22 Oct 2022 15:42:20 -0400 Subject: [PATCH 04/20] clean up --- s3s.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/s3s.py b/s3s.py index 3137765..1ddf165 100755 --- a/s3s.py +++ b/s3s.py @@ -586,12 +586,10 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): full_rank = re.split('([0-9]+)', child["udemae"].lower()) payload["rank_before"] = full_rank[0] - - if len(full_rank) > 1: - s_plus = True + s_plus_before = len(full_rank) > 1 #True if "before" rank is s+ + + if s_plus_before: payload["rank_before_s_plus"] = int(full_rank[1]) - else: - s_plus = False # anarchy battle (series) - not open if "bankaraMatchChallenge" in parent and parent["bankaraMatchChallenge"] is not None: @@ -607,7 +605,7 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if parent["bankaraMatchChallenge"]["udemaeAfter"] is None: payload["rank_after"] = payload["rank_before"] - if s_plus: + if s_plus_before: payload["rank_after_s_plus"] = payload["rank_before_s_plus"] else: if idx != 0: From c7447c23872e609453f5b094f1fc53a0176632f1 Mon Sep 17 00:00:00 2001 From: Austin <37423484+Toufool@users.noreply.github.com> Date: Sat, 22 Oct 2022 15:47:24 -0400 Subject: [PATCH 05/20] looks better like this --- s3s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3s.py b/s3s.py index 1ddf165..99f89af 100755 --- a/s3s.py +++ b/s3s.py @@ -585,9 +585,9 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if child["id"] == battle["id"]: # found the battle ID in the other file full_rank = re.split('([0-9]+)', child["udemae"].lower()) - payload["rank_before"] = full_rank[0] s_plus_before = len(full_rank) > 1 #True if "before" rank is s+ + payload["rank_before"] = full_rank[0] if s_plus_before: payload["rank_before_s_plus"] = int(full_rank[1]) From 91f8573c0812b238fc17299b0c260a9e34f63da4 Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sat, 22 Oct 2022 18:17:10 -1000 Subject: [PATCH 06/20] amend code & comments for #59 --- s3s.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/s3s.py b/s3s.py index 99f89af..c9e720e 100755 --- a/s3s.py +++ b/s3s.py @@ -585,10 +585,10 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if child["id"] == battle["id"]: # found the battle ID in the other file full_rank = re.split('([0-9]+)', child["udemae"].lower()) - s_plus_before = len(full_rank) > 1 #True if "before" rank is s+ + was_s_plus_before = len(full_rank) > 1 # true if "before" rank is s+ payload["rank_before"] = full_rank[0] - if s_plus_before: + if was_s_plus_before: payload["rank_before_s_plus"] = int(full_rank[1]) # anarchy battle (series) - not open @@ -605,8 +605,8 @@ def prepare_battle_result(battle, ismonitoring, overview_data=None): if parent["bankaraMatchChallenge"]["udemaeAfter"] is None: payload["rank_after"] = payload["rank_before"] - if s_plus_before: - payload["rank_after_s_plus"] = payload["rank_before_s_plus"] + if was_s_plus_before: + payload["rank_after_s_plus"] = payload["rank_before_s_plus"] # also s+ after else: if idx != 0: payload["rank_after"] = payload["rank_before"] From 678cd46361a7f951c64cac2ed6de79654a202be8 Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sat, 22 Oct 2022 18:53:54 -1000 Subject: [PATCH 07/20] fix -M when playing salmon run (#57) --- s3s.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/s3s.py b/s3s.py index c9e720e..c512fed 100755 --- a/s3s.py +++ b/s3s.py @@ -8,7 +8,7 @@ import argparse, datetime, json, os, shutil, re, requests, sys, time, uuid import msgpack import iksm, utils -A_VERSION = "0.1.7" +A_VERSION = "0.1.8" DEBUG = False @@ -991,7 +991,7 @@ def monitor_battles(which, secs, isblackout, istestrun): cached_battles.append(num) post_result(result, True, isblackout, istestrun) # True = is monitoring mode - if which in ("both", "salmon"): + if which in ("salmon"): # TODO - add in "both" when we have SR support for num in reversed(salmon_results): if num not in cached_jobs: # get the full job data @@ -1004,10 +1004,10 @@ def monitor_battles(which, secs, isblackout, istestrun): if False and utils.custom_key_exists("ignore_private"): # TODO - how to check for SR private battles? pass else: - outcome = "Success" if result["job_result"]["is_clear"] == True else "Failure" - if outcome == "Success": + outcome = "Clear" if result["data"]["coopHistoryDetail"]["resultWave"] == 0 else "Defeat" + if outcome == "Clear": job_successes += 1 - else: # Failure + else: job_failures += 1 stagename = result["data"]["coopHistoryDetail"]["coopStage"]["name"] @@ -1025,7 +1025,11 @@ def monitor_battles(which, secs, isblackout, istestrun): # check LatestBattleHistoriesQuery against https://stat.ink/api/v3/s3s/uuid-list - # TODO - do update_salmon_profile() at end if salmon run + # if SR: + # check CoopHistoryQuery + # update_salmon_profile() + + # show job/battle/splatfest_wins, etc. print("\n\nChecking for unuploaded results before exiting is not yet implemented.") print("Please run s3s again with " + '\033[91m' + "-r" + '\033[0m' + " to get these battles.") print("Bye!") From 36c8ca6ca6d6e99aa5a97d7f985e87d1df2432f6 Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sat, 22 Oct 2022 18:58:53 -1000 Subject: [PATCH 08/20] fix call to custom_key_exists() --- s3s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3s.py b/s3s.py index c512fed..2e761f9 100755 --- a/s3s.py +++ b/s3s.py @@ -1001,7 +1001,7 @@ def monitor_battles(which, secs, isblackout, istestrun): cookies=dict(_gtoken=GTOKEN)) result = json.loads(result_post.text) - if False and utils.custom_key_exists("ignore_private"): # TODO - how to check for SR private battles? + if False and utils.custom_key_exists("ignore_private", CONFIG_DATA): # TODO - how to check for SR private battles? pass else: outcome = "Clear" if result["data"]["coopHistoryDetail"]["resultWave"] == 0 else "Defeat" From f5b9e2e63e9da3575321c42a57d40c7ef4779114 Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sat, 22 Oct 2022 19:09:47 -1000 Subject: [PATCH 09/20] implement code style fixes (suggested in #51) --- s3s.py | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/s3s.py b/s3s.py index 2e761f9..c8b288e 100755 --- a/s3s.py +++ b/s3s.py @@ -38,6 +38,10 @@ 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 @@ -48,12 +52,8 @@ 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 -if "app_user_agent" in CONFIG_DATA: - APP_USER_AGENT = str(CONFIG_DATA["app_user_agent"]) -else: - APP_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)) + def write_config(tokens): '''Writes config file and updates the global variables.''' @@ -1055,18 +1055,9 @@ class SquidProgress: sys.stdout.flush() -def main(): - '''Main process, including I/O and setup.''' +def parse_arguments(): + '''Set up command-line options.''' - print('\033[93m\033[1m' + "s3s" + '\033[0m\033[93m' + f" v{A_VERSION}" + '\033[0m') - - # setup - ####### - check_for_updates() - check_statink_key() - - # argparse stuff - ################ parser = argparse.ArgumentParser() srgroup = parser.add_mutually_exclusive_group() parser.add_argument("-M", dest="N", required=False, nargs="?", action="store", @@ -1085,7 +1076,22 @@ def main(): help="upload local results. use `-i results.json overview.json`") parser.add_argument("-t", required=False, action="store_true", help="dry run for testing (won't post to stat.ink)") - parser_result = parser.parse_args() + return parser.parse_args() + + +def main(): + '''Main process, including I/O and setup.''' + + print('\033[93m\033[1m' + "s3s" + '\033[0m\033[93m' + f" v{A_VERSION}" + '\033[0m') + + # setup + ####### + check_for_updates() + check_statink_key() + + # argparse setup + ################ + parser_result = parse_arguments() # regular args n_value = parser_result.N @@ -1187,7 +1193,7 @@ def main(): try: statink_uploads = json.loads(resp.text) except: - print(f"Encountered an error while checking recently-uploaded {noun}. Is stat.ink down?") + print(f"Encountered an error while checking recently-uploaded data. Is stat.ink down?") sys.exit(1) to_upload = [] From 26ca08b4f5061a6b6fe1247cbe9effbed3bd755b Mon Sep 17 00:00:00 2001 From: Sou Niyari Date: Sun, 23 Oct 2022 14:26:34 +0900 Subject: [PATCH 10/20] add guards for directly-executed modules (#37) Co-authored-by: eli fessler --- iksm.py | 9 +++++++-- utils.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/iksm.py b/iksm.py index 6371c21..b7d3102 100644 --- a/iksm.py +++ b/iksm.py @@ -2,8 +2,8 @@ # https://github.com/frozenpandaman/s3s # License: GPLv3 -import requests, json, re, sys -import os, base64, hashlib +import base64, hashlib, json, os, re, sys +import requests from bs4 import BeautifulSoup session = requests.Session() @@ -379,3 +379,8 @@ def enter_tokens(): new_bullettoken = input("Invalid token - length should be 124 characters. Try again.\nEnter your bulletToken: ") return new_gtoken, new_bullettoken + + +if __name__ == "__main__": + print("This program cannot be run alone. See https://github.com/frozenpandaman/s3s") + sys.exit(0) diff --git a/utils.py b/utils.py index d133eae..6fb74c7 100644 --- a/utils.py +++ b/utils.py @@ -2,7 +2,8 @@ # https://github.com/frozenpandaman/s3s # License: GPLv3 -import base64, datetime, json, re, requests, uuid +import base64, datetime, json, re, sys, uuid +import requests from bs4 import BeautifulSoup SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net" @@ -103,3 +104,8 @@ def custom_key_exists(key, config_data, value=True): if key not in ["ignore_private", "app_user_agent", "force_uploads"]: print("(!) Checking unexpected custom key") return True if key in config_data and config_data[key].lower() == str(value).lower() else False + + +if __name__ == "__main__": + print("This program cannot be run alone. See https://github.com/frozenpandaman/s3s") + sys.exit(0) From 2210b1a66f948bbca48fba5ed36094d833517dab Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sun, 23 Oct 2022 01:39:50 -1000 Subject: [PATCH 11/20] amend message at start --- s3s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3s.py b/s3s.py index c8b288e..4072909 100755 --- a/s3s.py +++ b/s3s.py @@ -1231,7 +1231,7 @@ def main(): '\033[91m' + "-i results.json overview.json" + '\033[0m' + ". " \ "Or, run the script in monitoring mode (with " + '\033[91m' + "-M" + '\033[0m' \ ") to capture & upload new results as you play. " \ - "stat.ink does not support Salmon Run data (coop_results.json) or Splatfest battles at this time.\n") + "stat.ink does not support Salmon Run data (coop_results.json) at this time.\n") # --- if which in ("salmon", "both"): From 702637c082e3407cde66c38a2087970c301caaaf Mon Sep 17 00:00:00 2001 From: eli fessler Date: Mon, 24 Oct 2022 22:43:03 -1000 Subject: [PATCH 12/20] update splatnet version string --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 6fb74c7..c2987b8 100644 --- a/utils.py +++ b/utils.py @@ -8,7 +8,7 @@ from bs4 import BeautifulSoup SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net" GRAPHQL_URL = "https://api.lp1.av5ja.srv.nintendo.net/api/graphql" -WEB_VIEW_VERSION = "1.0.0-216d0219" # fallback +WEB_VIEW_VERSION = "1.0.0-5644e7a2" # fallback S3S_NAMESPACE = uuid.UUID('b3a2dbf5-2c09-4792-b78c-00b548b70aeb') # SHA256 hash database for SplatNet 3 GraphQL queries From e6cfacf099f5a7f01da0323f5928c94d6009d203 Mon Sep 17 00:00:00 2001 From: Sou Niyari Date: Wed, 26 Oct 2022 13:45:19 +0900 Subject: [PATCH 13/20] call improvements & web request refactoring (#38) Co-authored-by: eli fessler --- iksm.py | 7 ++++--- s3s.py | 13 +++++++++---- utils.py | 54 +++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/iksm.py b/iksm.py index b7d3102..e5b01f1 100644 --- a/iksm.py +++ b/iksm.py @@ -37,7 +37,7 @@ def get_nsoapp_version(): return NSOAPP_VERSION -def log_in(ver): +def log_in(ver, app_user_agent): '''Logs in to a Nintendo Account and returns a session_token.''' global S3S_VERSION @@ -55,7 +55,7 @@ def log_in(ver): 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0', 'Upgrade-Insecure-Requests': '1', - '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', + 'User-Agent': app_user_agent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n', 'DNT': '1', 'Accept-Encoding': 'gzip,deflate,br', @@ -301,7 +301,8 @@ def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_ 'X-Requested-With': 'com.nintendo.znca' } app_cookies = { - '_gtoken': web_service_token # X-GameWebToken + '_gtoken': web_service_token, # X-GameWebToken + '_dnt': '1' # Do Not Track } url = "https://api.lp1.av5ja.srv.nintendo.net/api/bullet_tokens" r = requests.post(url, headers=app_head, cookies=app_cookies) diff --git a/s3s.py b/s3s.py index 4072909..02b40ba 100755 --- a/s3s.py +++ b/s3s.py @@ -8,7 +8,8 @@ import argparse, datetime, json, os, shutil, re, requests, sys, time, uuid import msgpack import iksm, utils -A_VERSION = "0.1.8" +A_VERSION = "0.1.9" +WEB_VIEW_VERSION = "unknown" # set in prefetch_checks() DEBUG = False @@ -89,7 +90,7 @@ 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': utils.get_web_view_ver(), + 'X-Web-View-Ver': WEB_VIEW_VERSION, 'Content-Type': 'application/json', 'Accept': '*/*', 'Origin': 'https://api.lp1.av5ja.srv.nintendo.net', @@ -105,9 +106,13 @@ def prefetch_checks(printout=False): if printout: print("Validating your tokens...", end='\r') + 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: @@ -134,7 +139,7 @@ def gen_new_tokens(reason, force=False): if SESSION_TOKEN == "": print("Please log in to your Nintendo Account to obtain your session_token.") - new_token = iksm.log_in(A_VERSION) + new_token = iksm.log_in(A_VERSION, APP_USER_AGENT) if new_token is None: print("There was a problem logging you in. Please try again later.") elif new_token == "skip": @@ -155,7 +160,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, utils.get_web_view_ver(), APP_USER_AGENT, acc_lang, acc_country) + new_bullettoken = iksm.get_bullet(new_gtoken, WEB_VIEW_VERSION, 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 diff --git a/utils.py b/utils.py index c2987b8..975f939 100644 --- a/utils.py +++ b/utils.py @@ -7,8 +7,8 @@ import requests from bs4 import BeautifulSoup SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net" -GRAPHQL_URL = "https://api.lp1.av5ja.srv.nintendo.net/api/graphql" -WEB_VIEW_VERSION = "1.0.0-5644e7a2" # fallback +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 S3S_NAMESPACE = uuid.UUID('b3a2dbf5-2c09-4792-b78c-00b548b70aeb') # SHA256 hash database for SplatNet 3 GraphQL queries @@ -24,22 +24,58 @@ translate_rid = { 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e', # SR / req "coopHistoryDetailId" - query2 } -def get_web_view_ver(): +def get_web_view_ver(bhead, gtoken): '''Find & parse the SplatNet 3 main.js file for the current site version.''' - splatnet3_home = requests.get(SPLATNET3_URL) - soup = BeautifulSoup(splatnet3_home.text, "html.parser") + 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: - return WEB_VIEW_VERSION + + 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"] - main_js_body = requests.get(main_js_url) + + 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 match = re.search(r"\b(?P[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\d+\.\d+\.\d+)", main_js_body.text) if not match: - return WEB_VIEW_VERSION + return FALLBACK_WEB_VIEW_VERSION version, revision = match.group("version"), match.group("revision") return f"{version}-{revision[:8]}" From 1e2dfb421080ed15f87f0bf76599077b6b1f33cc Mon Sep 17 00:00:00 2001 From: eli fessler Date: Tue, 25 Oct 2022 19:27:52 -1000 Subject: [PATCH 14/20] minor code refactoring suggested by #43 --- utils.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/utils.py b/utils.py index 975f939..2dcea72 100644 --- a/utils.py +++ b/utils.py @@ -11,6 +11,12 @@ 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 S3S_NAMESPACE = uuid.UUID('b3a2dbf5-2c09-4792-b78c-00b548b70aeb') +SUPPORTED_KEYS = [ + "ignore_private", + "app_user_agent", + "force_uploads" +] + # SHA256 hash database for SplatNet 3 GraphQL queries # full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3737698 translate_rid = { @@ -21,7 +27,7 @@ translate_rid = { 'PrivateBattleHistoriesQuery': '38e0529de8bc77189504d26c7a14e0b8', # INK / blank vars - query1 'VsHistoryDetailQuery': '2b085984f729cd51938fc069ceef784a', # INK / req "vsResultId" - query2 'CoopHistoryQuery': '817618ce39bcf5570f52a97d73301b30', # SR / blank vars - query1 - 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e', # SR / req "coopHistoryDetailId" - query2 + 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e' # SR / req "coopHistoryDetailId" - query2 } def get_web_view_ver(bhead, gtoken): @@ -73,8 +79,9 @@ def get_web_view_ver(bhead, gtoken): if main_js_body.status_code != 200: return FALLBACK_WEB_VIEW_VERSION - match = re.search(r"\b(?P[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\d+\.\d+\.\d+)", main_js_body.text) - if not match: + pattern = r"\b(?P[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\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") @@ -137,9 +144,9 @@ def custom_key_exists(key, config_data, value=True): '''Checks if a given custom key exists in config.txt and is set to the specified value (true by default).''' # https://github.com/frozenpandaman/s3s/wiki/config-keys - if key not in ["ignore_private", "app_user_agent", "force_uploads"]: + if key not in SUPPORTED_KEYS: print("(!) Checking unexpected custom key") - return True if key in config_data and config_data[key].lower() == str(value).lower() else False + return str(config_data.get(key, None)).lower() == str(value).lower() if __name__ == "__main__": From 6628d657d774399658cd01959b9735cb59aa2414 Mon Sep 17 00:00:00 2001 From: eli fessler Date: Tue, 25 Oct 2022 20:40:44 -1000 Subject: [PATCH 15/20] avoid upload error by blanking out hacked grizzco weapons (#61) --- utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 2dcea72..727f598 100644 --- a/utils.py +++ b/utils.py @@ -105,9 +105,14 @@ def b64d(string): thing_id = base64.b64decode(string).decode('utf-8') thing_id = thing_id.replace("VsStage-", "") thing_id = thing_id.replace("VsMode-", "") - thing_id = thing_id.replace("Weapon-", "") thing_id = thing_id.replace("CoopStage-", "") thing_id = thing_id.replace("CoopGrade-", "") + + if "Weapon-" in thing_id: + thing_id = thing_id.replace("Weapon-", "") + if len(thing_id) == 5 and thing_id[:2] == "20": # grizzco weapon ID from a hacker + return "" + if thing_id[:15] == "VsHistoryDetail" or thing_id[:17] == "CoopHistoryDetail": return thing_id # string else: From fde9bb4823e12943304542d0ee205074054ed03d Mon Sep 17 00:00:00 2001 From: eli fessler Date: Tue, 25 Oct 2022 20:55:55 -1000 Subject: [PATCH 16/20] add prelim support for ignore_private used in SR --- s3s.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/s3s.py b/s3s.py index 02b40ba..2b1a6b2 100755 --- a/s3s.py +++ b/s3s.py @@ -673,7 +673,8 @@ def prepare_job_result(battle, ismonitoring, overview_data=None): '''Converts the Nintendo JSON format for a Salmon Run job to the stat.ink one.''' pass # stat.ink doesn't support SR yet - # combo of set_teammates() + salmon_post_shift() + # combo of set_teammates() + salmon_post_shift() - mirrors prepare_battle_result() + # specify if private or not; also ensure SR results are formatted the same in post_result()! # set payload["splatnet_json"] @@ -713,7 +714,7 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): continue # should have been taken care of in fetch_json() but just in case... - if payload["lobby"] == "private" and utils.custom_key_exists("ignore_private", CONFIG_DATA): # TODO - also check SR? + if payload["lobby"] == "private" and utils.custom_key_exists("ignore_private", CONFIG_DATA): continue # TODO - isblackout stuff... for SR too @@ -1006,7 +1007,8 @@ def monitor_battles(which, secs, isblackout, istestrun): cookies=dict(_gtoken=GTOKEN)) result = json.loads(result_post.text) - if False and utils.custom_key_exists("ignore_private", CONFIG_DATA): # TODO - how to check for SR private battles? + if result["data"]["coopHistoryDetail"]["jobScore"] == None \ + and utils.custom_key_exists("ignore_private", CONFIG_DATA): pass else: outcome = "Clear" if result["data"]["coopHistoryDetail"]["resultWave"] == 0 else "Defeat" From b94ecbc9dfb310c7710359f53c73e8b50c1d7cbd Mon Sep 17 00:00:00 2001 From: eli fessler Date: Tue, 25 Oct 2022 21:45:11 -1000 Subject: [PATCH 17/20] fix grizzco weapon ID detection --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 727f598..ad3b22c 100644 --- a/utils.py +++ b/utils.py @@ -110,7 +110,7 @@ def b64d(string): if "Weapon-" in thing_id: thing_id = thing_id.replace("Weapon-", "") - if len(thing_id) == 5 and thing_id[:2] == "20": # grizzco weapon ID from a hacker + if len(thing_id) == 5 and thing_id[:1] == "2" and thing_id[-3:] == "900": # grizzco weapon ID from a hacker return "" if thing_id[:15] == "VsHistoryDetail" or thing_id[:17] == "CoopHistoryDetail": From a4425bf461fa665ceb1f734085460ca44c70888e Mon Sep 17 00:00:00 2001 From: eli fessler Date: Wed, 26 Oct 2022 13:02:09 -1000 Subject: [PATCH 18/20] fix typo --- iksm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iksm.py b/iksm.py index e5b01f1..6acf73f 100644 --- a/iksm.py +++ b/iksm.py @@ -321,7 +321,7 @@ def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_ bullet_resp = json.loads(r.text) bullet_token = bullet_resp["bulletToken"] except (json.decoder.JSONDecodeError, TypeError): - print("Got non-JSON response from Nintendo (in api/bullet_tokens step:") + print("Got non-JSON response from Nintendo (in api/bullet_tokens step):") print(bullet_resp) sys.exit(1) except: From 487ab596da2195b14dc784d0cbeba398abd85d1e Mon Sep 17 00:00:00 2001 From: eli fessler Date: Wed, 26 Oct 2022 16:31:08 -1000 Subject: [PATCH 19/20] major refactor to address #62 & improve structure --- iksm.py | 148 ++++++++++++++++++++++++++++++++++++++++++------------- s3s.py | 26 +++++----- utils.py | 67 ++----------------------- 3 files changed, 130 insertions(+), 111 deletions(-) diff --git a/iksm.py b/iksm.py index 6acf73f..077c5db 100644 --- a/iksm.py +++ b/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[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\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: diff --git a/s3s.py b/s3s.py index 2b1a6b2..f60589c 100755 --- a/s3s.py +++ b/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 diff --git a/utils.py b/utils.py index ad3b22c..31f372c 100644 --- a/utils.py +++ b/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[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\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-", "") From ab0b8b3cd2502db84edb0be676b71701cd4cc4aa Mon Sep 17 00:00:00 2001 From: eli fessler Date: Wed, 26 Oct 2022 16:38:48 -1000 Subject: [PATCH 20/20] improve docstrings --- s3s.py | 17 +++++++++-------- utils.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/s3s.py b/s3s.py index f60589c..6ad51e2 100755 --- a/s3s.py +++ b/s3s.py @@ -82,7 +82,7 @@ def write_config(tokens): def headbutt(): - '''Return a (dynamic!) header used for GraphQL requests.''' + '''Returns a (dynamic!) header used for GraphQL requests.''' graphql_head = { 'Authorization': f'Bearer {BULLETTOKEN}', # update every time it's called with current global var @@ -100,7 +100,7 @@ def headbutt(): def prefetch_checks(printout=False): - '''Queries the SplatNet 3 homepage to check if our gtoken cookie and bulletToken are still valid, otherwise regenerate.''' + '''Queries the SplatNet 3 homepage to check if our gtoken & bulletToken are still valid and regenerates them if not.''' if printout: print("Validating your tokens...", end='\r') @@ -171,7 +171,7 @@ def gen_new_tokens(reason, force=False): def fetch_json(which, separate=False, exportall=False, specific=False, numbers_only=False, printout=False, skipprefetch=False): - '''Returns results JSON from SplatNet 3, including a combined dict for ink battles + SR jobs if requested.''' + '''Returns results JSON from SplatNet 3, including a combined dictionary for battles + SR jobs if requested.''' swim = SquidProgress() @@ -677,7 +677,7 @@ def prepare_job_result(battle, ismonitoring, overview_data=None): def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): - '''Uploads battle/job JSON to stat.ink, and prints the returned URL or error message..''' + '''Uploads battle/job JSON to stat.ink, and prints the returned URL or error message.''' if isinstance(data, list): # -o export format try: @@ -764,6 +764,7 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): def check_for_updates(): '''Checks the script version against the repo, reminding users to update if available.''' + # TODO print('\033[3m' + "ยป While s3s is in beta, please update the script regularly via " \ '`\033[91m' + "git pull" + '\033[0m' + "`." + '\033[0m' + "\n") # try: @@ -855,7 +856,7 @@ def fetch_and_upload_single_result(hash, noun, ismonitoring, isblackout, istestr def check_if_missing(which, ismonitoring, isblackout, istestrun): - '''Checks for unuploaded battles, and uploads any that are found.''' + '''Checks for unuploaded battles and uploads any that are found.''' noun = utils.set_noun(which) print(f"Checking if there are previously-unuploaded {noun}...") @@ -912,7 +913,7 @@ def check_if_missing(which, ismonitoring, isblackout, istestrun): def monitor_battles(which, secs, isblackout, istestrun): - '''Monitors JSON for changes/new battles and uploads them.''' + '''Monitors SplatNet endpoint(s) for changes (new results) and uploads them.''' if DEBUG: print(f"* monitoring mode start - calling fetch_json() w/ which={which}") @@ -1041,7 +1042,7 @@ def monitor_battles(which, secs, isblackout, istestrun): class SquidProgress: - '''Display animation while waiting.''' + '''Displays an animation of a squid swimming while waiting. :)''' def __init__(self): self.count = 0 @@ -1061,7 +1062,7 @@ class SquidProgress: def parse_arguments(): - '''Set up command-line options.''' + '''Setup for command-line options.''' parser = argparse.ArgumentParser() srgroup = parser.add_mutually_exclusive_group() diff --git a/utils.py b/utils.py index 31f372c..7f223c7 100644 --- a/utils.py +++ b/utils.py @@ -43,7 +43,7 @@ def set_noun(which): def b64d(string): - '''Base64 decodes a string and cut off the SplatNet prefix.''' + '''Base64-decodes a string and cuts off the SplatNet prefix.''' thing_id = base64.b64decode(string).decode('utf-8') thing_id = thing_id.replace("VsStage-", "") @@ -63,7 +63,7 @@ def b64d(string): def epoch_time(time_string): - '''Converts a playedTime string into an int representing the epoch time.''' + '''Converts a playedTime string into an integer representing the epoch time.''' utc_time = datetime.datetime.strptime(time_string, "%Y-%m-%dT%H:%M:%SZ") epoch_time = int((utc_time - datetime.datetime(1970, 1, 1)).total_seconds())