From 4fc712ede4aa895e3de28775e9d370a318aa62da Mon Sep 17 00:00:00 2001 From: eli fessler Date: Fri, 25 Nov 2022 10:41:18 -0600 Subject: [PATCH] fix merge conflicts --- s3s.py | 463 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 363 insertions(+), 100 deletions(-) diff --git a/s3s.py b/s3s.py index cbcef02..6c6668c 100644 --- a/s3s.py +++ b/s3s.py @@ -6,11 +6,12 @@ import argparse, base64, datetime, json, os, shutil, re, requests, sys, time, uuid from concurrent.futures import ThreadPoolExecutor -import mmh3, msgpack +from subprocess import call +import msgpack from packaging import version import iksm, utils -A_VERSION = "0.1.17" +A_VERSION = "0.2.0" DEBUG = False @@ -85,19 +86,26 @@ def write_config(tokens): config_file.close() -def headbutt(): +def headbutt(forcelang=None): '''Returns a (dynamic!) header used for GraphQL requests.''' + if forcelang: + lang = forcelang + country = forcelang[-2:] + else: + lang = USER_LANG + country = USER_COUNTRY + graphql_head = { 'Authorization': f'Bearer {BULLETTOKEN}', # update every time it's called with current global var - 'Accept-Language': USER_LANG, + 'Accept-Language': lang, 'User-Agent': APP_USER_AGENT, 'X-Web-View-Ver': iksm.get_web_view_ver(), 'Content-Type': 'application/json', 'Accept': '*/*', 'Origin': iksm.SPLATNET3_URL, 'X-Requested-With': 'com.nintendo.znca', - 'Referer': f'{iksm.SPLATNET3_URL}?lang={USER_LANG}&na_country={USER_COUNTRY}&na_lang={USER_LANG}', + 'Referer': f'{iksm.SPLATNET3_URL}?lang={lang}&na_country={country}&na_lang={lang}', 'Accept-Encoding': 'gzip, deflate' } return graphql_head @@ -228,10 +236,14 @@ def fetch_json(which, separate=False, exportall=False, specific=False, numbers_o if sha is not None: if DEBUG: print(f"* making query1 to {sha}") + lang = 'en-US' if sha == "CoopHistoryQuery" else None sha = utils.translate_rid[sha] battle_ids, job_ids = [], [] - query1 = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) + query1 = requests.post(utils.GRAPHQL_URL, + data=utils.gen_graphql_body(sha), + headers=headbutt(forcelang=lang), + cookies=dict(_gtoken=GTOKEN)) query1_resp = json.loads(query1.text) swim() @@ -270,7 +282,6 @@ def fetch_json(which, separate=False, exportall=False, specific=False, numbers_o ink_list.extend(battle_ids) salmon_list.extend(job_ids) else: # ALL DATA - TAKES A LONG TIME - ink_list.extend(thread_pool.map(fetch_detailed_result, [True]*len(battle_ids), battle_ids, [swim]*len(battle_ids))) salmon_list.extend(thread_pool.map(fetch_detailed_result, [False]*len(job_ids), job_ids, [swim]*len(job_ids))) @@ -300,26 +311,33 @@ def fetch_json(which, separate=False, exportall=False, specific=False, numbers_o return combined def fetch_detailed_result(is_vs_history, history_id, swim): + '''Helper function for fetch_json().''' + sha = "VsHistoryDetailQuery" if is_vs_history else "CoopHistoryDetailQuery" varname = "vsResultId" if is_vs_history else "coopHistoryDetailId" + lang = None if is_vs_history else 'en-US' + query2 = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(utils.translate_rid[sha], varname, history_id), - headers=headbutt(), + headers=headbutt(forcelang=lang), cookies=dict(_gtoken=GTOKEN)) query2_resp = json.loads(query2.text) + swim() return query2_resp + def update_salmon_profile(): ''' Updates stat.ink Salmon Run stats/profile.''' - pass + pass # SR TODO # prefetch_checks() - # old code - need stat.ink s3 support - TODO - # url = nintendo's api/coop_results... - # results_list = requests.get(url, headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) + # results_list = requests.post(utils.GRAPHQL_URL, + # data=utils.gen_graphql_body(utils.translate_rid["CoopHistoryQuery"]), + # headers=headbutt(forcelang='en-US'), + # cookies=dict(_gtoken=GTOKEN)) # data = json.loads(results_list.text) # profile = data["summary"] @@ -329,10 +347,10 @@ def update_salmon_profile(): # "total_eggs": profile["card"]["ikura_total"], # "total_rescued": profile["card"]["help_total"], # "total_point": profile["card"]["kuma_point_total"] - # # TODO - other new things - fish scales, xtrawave stuff?, etc. + # # other new things - total fish scales, kings defeated, current & total points? # } - # url = "https://stat.ink/api/v2/salmon-stats" # TODO - need stat.ink s3 support + # url = "https://stat.ink/api/v2/salmon-stats" # auth = {'Authorization': f'Bearer {API_KEY}'} # updateprofile = requests.post(url, headers=auth, data=payload) @@ -598,7 +616,9 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): payload["knockout"] = "no" if battle["knockout"] is None or battle["knockout"] == "NEITHER" else "yes" payload["rank_exp_change"] = battle["bankaraMatch"]["earnedUdemaePoint"] - battle_id = base64.b64decode(battle["id"]).decode('utf-8') + battle_id = base64.b64decode(battle["id"]).decode('utf-8') + battle_id_mutated = battle_id.replace("BANKARA", "RECENT") # normalize the ID, make work with -M and -r + if overview_data is None: # no passed in file with -i overview_post = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(utils.translate_rid["BankaraBattleHistoriesQuery"]), @@ -620,13 +640,14 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): for parent in ranked_list: # groups in overview (ranked) JSON/screen for idx, child in enumerate(parent["historyDetails"]["nodes"]): - overview_battle_id = base64.b64decode(child["id"]).decode('utf-8') + overview_battle_id = base64.b64decode(child["id"]).decode('utf-8') overview_battle_id_mutated = overview_battle_id.replace("BANKARA", "RECENT") # same battle, different screens - if overview_battle_id_mutated == battle_id: # found the battle ID in the other file + + if overview_battle_id_mutated == battle_id_mutated: # found the battle ID in the other file full_rank = re.split('([0-9]+)', child["udemae"].lower()) was_s_plus_before = len(full_rank) > 1 # true if "before" rank is s+ - + payload["rank_before"] = full_rank[0] if was_s_plus_before: payload["rank_before_s_plus"] = int(full_rank[1]) @@ -643,13 +664,11 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): else: payload["rank_up_battle"] = "no" - if parent["bankaraMatchChallenge"]["udemaeAfter"] is None: - payload["rank_after"] = payload["rank_before"] - if was_s_plus_before: - payload["rank_after_s_plus"] = payload["rank_before_s_plus"] # also s+ after - else: + if parent["bankaraMatchChallenge"]["udemaeAfter"] is not None: if idx != 0: payload["rank_after"] = payload["rank_before"] + if was_s_plus_before: # not a rank-up battle, so must be the same + payload["rank_after_s_plus"] = payload["rank_before_s_plus"] else: # the battle where we actually ranked up full_rank_after = re.split('([0-9]+)', parent["bankaraMatchChallenge"]["udemaeAfter"].lower()) payload["rank_after"] = full_rank_after[0] @@ -729,13 +748,233 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): return payload -def prepare_job_result(battle, ismonitoring, isblackout, overview_data=None): +def prepare_job_result(job, ismonitoring, isblackout, 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() - mirrors prepare_battle_result() - # specify if private or not; also ensure SR results are formatted the same in post_result()! - # set payload["splatnet_json"] + # https://github.com/fetus-hina/stat.ink/wiki/Spl3-API:-Salmon-%EF%BC%8D-Post + payload = {} + job = job["coopHistoryDetail"] + + full_id = utils.b64d(job["id"]) + payload["uuid"] = str(uuid.uuid5(utils.SALMON_NAMESPACE, full_id[-52:])) + + payload["stage"] = utils.b64d(job["coopStage"]["id"]) + + payload["danger_rate"] = job["dangerRate"] * 100 + payload["king_smell"] = job["smellMeter"] + + waves_cleared = job["resultWave"] - 1 # resultWave = 0 if all normal waves cleared + payload["clear_waves"] = 3 if waves_cleared == -1 else waves_cleared + + if payload["clear_waves"] < 0: # player dc'd + payload["clear_waves"] = None + + elif payload["clear_waves"] != 3: # job defeat only + underperformed = False + for wave in job["waveResults"]: + if wave["teamDeliverCount"] < wave["deliverNorm"]: + underperformed = True + payload["fail_reason"] = "time_limit" if underperformed else "wipe_out" + + # xtrawave only + # https://stat.ink/api-info/boss-salmonid3 + if job["bossResult"]: + king_id = utils.b64d(job["bossResult"]["boss"]["id"]) + if king_id == 23: # cohozuna + payload["king_salmonid"] = "yokozuna" + + payload["clear_extra"] = "yes" if job["bossResult"]["hasDefeatBoss"] else "no" + + # https://stat.ink/api-info/salmon-title3 + payload["title_after"] = utils.b64d(job["afterGrade"]["id"]) + payload["title_exp_after"] = job["afterGradePoint"] + + point_diff = 20 if payload["clear_waves"] == 3 else -30 + (10 * job["resultWave"]) # +20 for win or -(30-10w) for loss + if payload["title_exp_after"] - point_diff >= 0: # before exp isn't negative, i.e. no title change + payload["title_before"] = payload["title_after"] + payload["title_exp_before"] = payload["title_exp_after"] - point_diff + else: # ranked up + payload["title_before"] = payload["title_after"] - 1 + # exp... + + geggs = job["myResult"]["goldenDeliverCount"] + peggs = job["myResult"]["deliverCount"] + for player in job["memberResults"]: + geggs += player["goldenDeliverCount"] + peggs += player["deliverCount"] + payload["golden_eggs"] = geggs + payload["power_eggs"] = peggs + + if job["scale"]: + payload["gold_scale"] = job["scale"]["gold"] + payload["silver_scale"] = job["scale"]["silver"] + payload["bronze_scale"] = job["scale"]["bronze"] + + payload["job_score"] = job["jobScore"] # job score + payload["job_rate"] = job["jobRate"] # pay grade + payload["job_bonus"] = job["jobBonus"] # clear bonus + payload["job_point"] = job["jobPoint"] # your points = floor((score x rate) + bonus) + # note the current bug with "bonus" lol... https://github.com/frozenpandaman/s3s/wiki/%7C-splatnet-bugs + + waves = [] + for wave in job["waveResults"]: + wave_info = {} + wave_info["tide"] = "low" if wave["waterLevel"] == 0 else "high" if wave["waterLevel"] == 2 else "normal" + wave_info["golden_quota"] = wave["deliverNorm"] + wave_info["golden_delivered"] = wave["teamDeliverCount"] + wave_info["golden_appearances"] = wave["goldenPopCount"] + + if wave["eventWave"]: + event_id = utils.b64d(wave["eventWave"]["id"]) + translate_occurrence = { + 1: "rush", + 2: "goldie_seeking", + 3: "the_griller", + 4: "the_mothership", + 5: "fog", + 6: "cohock_charge", + 7: "giant_tornado", + 8: "mudmouth_eruption" + } + wave_info["event"] = translate_occurrence[event_id] + + translate_special = { + 20006: "nicedama", + 20007: "hopsonar", + 20009: "megaphone51", + 20010: "jetpack", + 20012: "kanitank", + 20013: "sameride", + 20014: "tripletornado" + } + special_uses = { + "nicedama": 0, + "hopsonar": 0, + "megaphone51": 0, + "jetpack": 0, + "kanitank": 0, + "sameride": 0, + "tripletornado": 0, + "unknown": 0 + } + for wep_use in wave["specialWeapons"]: + special_id = utils.b64d(wep_use["id"]) + special_key = translate_special.get(special_id, "unknown") + special_uses[special_key] += 1 # increment value of the key each time it's found + wave_info["special_uses"] = special_uses + + waves.append(wave_info) + payload["waves"] = waves + + players = [] + players_json = [job["myResult"]] + for teammate in job["memberResults"]: + players_json.append(teammate) + + for player in players_json: + player_info = {} + player_info["me"] = "yes" if player["player"]["isMyself"] else "no" + player_info["name"] = player["player"]["name"] + player_info["number"] = player["player"]["nameId"] + player_info["splashtag_title"] = player["player"]["byname"] + player_info["golden_eggs"] = player["goldenDeliverCount"] + player_info["golden_assist"] = player["goldenAssistCount"] + player_info["power_eggs"] = player["deliverCount"] + player_info["rescue"] = player["rescueCount"] + player_info["rescued"] = player["rescuedCount"] + player_info["defeat_boss"] = player["defeatEnemyCount"] + + dc_indicators = [ + player_info["golden_eggs"], + player_info["power_eggs"], + player_info["rescue"], + player_info["rescued"], + player_info["defeat_boss"] + ] + player_info["disconnected"] = "yes" if all(value == 0 for value in dc_indicators) else "no" + + slop_num = utils.b64d(player["player"]["uniform"]["id"]) + translate_slop = { + 1: "orange", + 2: "green", + 3: "yellow", + 4: "pink", + 5: "blue", + 6: "black", + 7: "white" + } + player_info["uniform"] = translate_slop[slop_num] + + try: + special_id = utils.b64d(player["specialWeapon"]["id"]) + player_info["special"] = translate_special[special_id] + except TypeError: # player.specialWeapon is null - player dc'd + pass + except KeyError: # invalid special weapon - likely defaulted to '1' before it could be assigned + pass + + weapons = [] + for weapon in player["weapons"]: # should always be returned in in english due to headbutt() using forcelang + wep_string = weapon["name"].lower().replace(" ", "_").replace("-", "_").replace(".", "").replace("'", "") + if wep_string == "random": # NINTENDOOOOOOO + wep_string = None + weapons.append(wep_string) + player_info["weapons"] = weapons + + players.append(player_info) + payload["players"] = players + + # https://stat.ink/api-info/boss-salmonid3 + bosses = {} + translate_boss = { + 4: "bakudan", + 5: "katapad", + 6: "teppan", + 7: "hebi", + 8: "tower", + 9: "mogura", + 10: "koumori", + 11: "hashira", + 12: "diver", + 13: "tekkyu", + 14: "nabebuta", + 15: "kin_shake", + 17: "grill", + 20: "doro_shake" + } + for boss in job["enemyResults"]: + boss_id = utils.b64d(boss["enemy"]["id"]) + boss_key = translate_boss[boss_id] + bosses[boss_key] = { + "appearances": boss["popCount"], + "defeated": boss["teamDefeatCount"], + "defeated_by_me": boss["defeatCount"] + } + payload["bosses"] = bosses + + payload["start_at"] = utils.epoch_time(job["playedTime"]) + payload["private"] = "yes" if not job["jobPoint"] else "no" + + payload["big_run"] = "no" # SR TODO + + if isblackout: + # fix payload + for player in payload["players"]: + if player["me"] == "no": + player["name"] = None + player["number"] = None + player["splashtag_title"] = None + + # fix job json + for player in job["memberResults"]: + player["player"]["name"] = None + player["player"]["nameId"] = None + player["player"]["byname"] = None + + payload["splatnet_json"] = json.dumps(job) + payload["automated"] = "yes" + + return payload def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): @@ -747,12 +986,12 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): if isinstance(data, list): # -o export format try: - data = [x for x in data if x['data']['vsHistoryDetail'] is not None] # avoid {'data': {'vsHistoryDetail': None}} error - results = sorted(data, key=lambda d: d['data']['vsHistoryDetail']['playedTime']) + data = [x for x in data if x["data"]["vsHistoryDetail"] is not None] # avoid {"data": {"vsHistoryDetail": None}} error + results = sorted(data, key=lambda d: d["data"]["vsHistoryDetail"]["playedTime"]) except KeyError: try: - data = [x for x in data if x['coopHistoryDetail'] is not None] - results = sorted(data, key=lambda d: d['data']['coopHistoryDetail']['playedTime']) + data = [x for x in data if x["data"]["coopHistoryDetail"] is not None] + results = sorted(data, key=lambda d: d["data"]["coopHistoryDetail"]["playedTime"]) except KeyError: # unsorted - shouldn't happen print("(!) Uploading without chronologically sorting results") results = data @@ -766,8 +1005,10 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): for i in range(len(results)): if "vsHistoryDetail" in results[i]["data"]: # ink battle payload = prepare_battle_result(results[i]["data"], ismonitoring, isblackout, overview_data) + which = "ink" elif "coopHistoryDetail" in results[i]["data"]: # salmon run job payload = prepare_job_result(results[i]["data"], ismonitoring, isblackout, overview_data) + which = "salmon" else: # shouldn't happen print("Ill-formatted JSON while uploading. Exiting.") print('\nDebug info:') @@ -778,7 +1019,8 @@ 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): + if payload.get("lobby") == "private" and utils.custom_key_exists("ignore_private", CONFIG_DATA) or \ + payload.get("private") == "yes" and utils.custom_key_exists("ignore_private", CONFIG_DATA): # SR version continue s3s_values = {'agent': '\u0073\u0033\u0073', 'agent_version': f'v{A_VERSION}'} # lol @@ -792,8 +1034,12 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): if istestrun: payload["test"] = "yes" - # post - url = "https://stat.ink/api/v3/battle" + # POST + url = "https://stat.ink/api/v3" + if which == "ink": + url += "/battle" + elif which == "salmon": + url += "/salmon" auth = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/x-msgpack'} postbattle = requests.post(url, headers=auth, data=msgpack.packb(payload), allow_redirects=False) @@ -817,12 +1063,12 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): print(f"* time uploaded: {time_uploaded}; time now: {time_now}") if postbattle.status_code != 201: # Created (or already exists) - print("Error uploading battle. Message from server:") + print(f"Error uploading {utils.set_noun(which)[:-1]}. Message from server:") print(postbattle.content.decode('utf-8')) elif time_uploaded <= time_now - 5: # give some leeway - print(f"Battle already uploaded - {headerloc}") + print(f"{utils.set_noun(which)[:-1].capitalize()} already uploaded - {headerloc}") else: # 200 OK - print(f"Battle uploaded to {headerloc}") + print(f"{utils.set_noun(which)[:-1].capitalize()} uploaded to {headerloc}") def check_for_updates(): @@ -849,7 +1095,7 @@ def check_for_updates(): "` as soon as possible.\n") else: # no git directory print(" Visit the site below to update:\nhttps://github.com/frozenpandaman/s3s\n") - except: # if there's a problem connecting to github + except Exception as e: # if there's a problem connecting to github print('\033[3m' + "» Couldn't connect to GitHub. Please update the script manually via " \ '`\033[91m' + "git pull" + '\033[0m' + "`." + '\033[0m' + "\n") # print('\033[3m' + "» While s3s is in beta, please update the script regularly via " \ @@ -928,24 +1174,26 @@ def get_num_results(which): def fetch_and_upload_single_result(hash, noun, isblackout, istestrun): - '''Performs a GraphQL request for a single vsResultId/coopResultId and call post_result().''' + '''Performs a GraphQL request for a single vsResultId/coopHistoryDetailId and call post_result().''' if noun in ("battles", "battle"): dict_key = "VsHistoryDetailQuery" dict_key2 = "vsResultId" + lang = None else: # noun == "jobs" or "job" - dict_key = "CoopHistoryQuery" - dict_key2 = "coopResultId" + dict_key = "CoopHistoryDetailQuery" + dict_key2 = "coopHistoryDetailId" + lang = 'en-US' result_post = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(utils.translate_rid[dict_key], dict_key2, hash), - headers=headbutt(), + headers=headbutt(forcelang=lang), cookies=dict(_gtoken=GTOKEN)) result = json.loads(result_post.text) post_result(result, False, isblackout, istestrun) # not monitoring mode -def check_if_missing(which, isblackout, istestrun): +def check_if_missing(which, isblackout, istestrun, skipprefetch): '''Checks for unuploaded battles and uploads any that are found (-r flag).''' noun = utils.set_noun(which) @@ -958,10 +1206,10 @@ def check_if_missing(which, isblackout, istestrun): urls.append("https://stat.ink/api/v3/s3s/uuid-list") # max 200 entries else: urls.append(None) - # if which == "both" or which == "salmon": - # urls.append("...") - # else: - # urls.append(None) + if which == "both" or which == "salmon": + urls.append("https://stat.ink/api/v3/salmon/uuid-list") + else: + urls.append(None) noun = "battles" # first (and maybe only) which = "ink" @@ -978,19 +1226,27 @@ def check_if_missing(which, isblackout, istestrun): # ! fetch from online # specific - check ALL possible battles; printout - to show tokens are being checked at program start - splatnet_ids = fetch_json(which, specific=True, numbers_only=True, printout=True) + splatnet_ids = fetch_json(which, specific=True, numbers_only=True, printout=True, skipprefetch=skipprefetch) # same as code in -i section below... for id in reversed(splatnet_ids): full_id = utils.b64d(id) - old_uuid = full_id[-36:] - new_uuid = str(uuid.uuid5(utils.S3S_NAMESPACE, full_id[-52:])) - if new_uuid in statink_uploads: - continue - if old_uuid in statink_uploads: - if not utils.custom_key_exists("force_uploads", CONFIG_DATA): + if which == "ink": + old_battle_uuid = full_id[-36:] + new_battle_uuid = str(uuid.uuid5(utils.S3S_NAMESPACE, full_id[-52:])) + + if new_battle_uuid in statink_uploads: continue + if old_battle_uuid in statink_uploads: + if not utils.custom_key_exists("force_uploads", CONFIG_DATA): + continue + + elif which == "salmon": + job_uuid = str(uuid.uuid5(utils.SALMON_NAMESPACE, full_id[-52:])) + if job_uuid in statink_uploads: + continue + if not printed: printed = True print(f"Previously-unuploaded {noun} detected. Uploading now...") @@ -1003,25 +1259,9 @@ def check_if_missing(which, isblackout, istestrun): noun = "jobs" # for second run through the loop which = "salmon" -def check_for_new_results(input_params): +def check_for_new_results(which, cached_battles, cached_jobs, battle_wins, battle_losses, battle_draws, splatfest_wins, splatfest_losses, splatfest_draws, mirror_matches, job_successes, job_failures, isblackout, istestrun): '''Helper function for monitor_battles(), called every N seconds or when exiting.''' - # INPUT PARAMETERS, IN ORDER: - which = input_params[0] # "ink", "salmon", or "both" - cached_battles = input_params[1] # lists of battles/jobs monitoring mode already knows about - cached_jobs = input_params[2] - battle_wins = input_params[3] # ink battle counters - battle_losses = input_params[4] - battle_draws = input_params[5] - splatfest_wins = input_params[6] # splatfest counters - splatfest_losses = input_params[7] - splatfest_draws = input_params[8] - mirror_matches = input_params[9] - job_successes = input_params[10] # salmon run counters - job_failures = input_params[11] - isblackout = input_params[12] # type of run - istestrun = input_params[13] - # ! fetch from online # check only numbers (quicker); specific=False since checks recent (latest) only ink_results, salmon_results = fetch_json(which, separate=True, numbers_only=True) @@ -1085,8 +1325,8 @@ def check_for_new_results(input_params): if num not in cached_jobs: # get the full job data result_post = requests.post(utils.GRAPHQL_URL, - data=utils.gen_graphql_body(utils.translate_rid["CoopHistoryDetailQuery"], "coopResultId", num), - headers=headbutt(), + data=utils.gen_graphql_body(utils.translate_rid["CoopHistoryDetailQuery"], "coopHistoryDetailId", num), + headers=headbutt(forcelang='en-US'), cookies=dict(_gtoken=GTOKEN)) result = json.loads(result_post.text) @@ -1103,8 +1343,7 @@ def check_for_new_results(input_params): stagename = result["data"]["coopHistoryDetail"]["coopStage"]["name"] shortname = stagename.split(" ")[-1] # fine for salmon run stage names too - endtime = utils.epoch_time(result["data"]["coopHistoryDetail"]["playedTime"]) + \ - result["data"]["coopHistoryDetail"]["duration"] + endtime = utils.epoch_time(result["data"]["coopHistoryDetail"]["playedTime"]) dt = datetime.datetime.fromtimestamp(endtime).strftime('%I:%M:%S %p').lstrip("0") print(f"New job result detected at {dt}! ({shortname}, {outcome})") @@ -1153,7 +1392,7 @@ def monitor_battles(which, secs, isblackout, istestrun, skipprefetch): job_successes, job_failures, isblackout, istestrun ] - which, cached_battles, cached_jobs, battle_wins, battle_losses, battle_draws, splatfest_wins, splatfest_losses, splatfest_draws, mirror_matches, job_successes, job_failures, foundany = check_for_new_results(input_params) + which, cached_battles, cached_jobs, battle_wins, battle_losses, battle_draws, splatfest_wins, splatfest_losses, splatfest_draws, mirror_matches, job_successes, job_failures, foundany = check_for_new_results(*input_params) except KeyboardInterrupt: print(f"\n\nChecking to see if there are unuploaded {utils.set_noun(which)} before exiting...") @@ -1166,7 +1405,7 @@ def monitor_battles(which, secs, isblackout, istestrun, skipprefetch): job_successes, job_failures, isblackout, istestrun ] - which, cached_battles, cached_jobs, battle_wins, battle_losses, battle_draws, splatfest_wins, splatfest_losses, splatfest_draws, mirror_matches, job_successes, job_failures, foundany = check_for_new_results(input_params) + which, cached_battles, cached_jobs, battle_wins, battle_losses, battle_draws, splatfest_wins, splatfest_losses, splatfest_draws, mirror_matches, job_successes, job_failures, foundany = check_for_new_results(*input_params) noun = utils.set_noun(which) if foundany: @@ -1197,10 +1436,9 @@ def monitor_battles(which, secs, isblackout, istestrun, skipprefetch): print(f"{mirror_matches} mirror match{'' if mirror_matches == 1 else 'es'} against your Splatfest team.") - # SR TODO - # if which in ("salmon", "both"): - # print(f"Salmon Run: {job_successes} success{'' if job_successes == 1 else 'es'} and " \ - # f"{job_failures} failure{'' if job_failures == 1 else 's'}.") + if which in ("salmon", "both"): + print(f"Salmon Run: {job_successes} success{'' if job_successes == 1 else 'es'} and " \ + f"{job_failures} failure{'' if job_failures == 1 else 's'}.") print("Bye!") @@ -1228,6 +1466,13 @@ class SquidProgress: def export_seed_json(skipprefetch=False): '''Export a JSON file for use with Lean's seed checker at https://leanny.github.io/splat3seedchecker/.''' + try: + import pymmh3 as mmh3 + except ModuleNotFoundError: + print("This function requires a Python module you don't have installed. " \ + "Please run " + '`\033[91m' + "pip install -r requirements.txt" + '\033[0m`' + " and try again.") + sys.exit(1) + if not skipprefetch: prefetch_checks(printout=True) @@ -1304,7 +1549,7 @@ def parse_arguments(): parser.add_argument("-o", required=False, action="store_true", help="export all possible results to local files") parser.add_argument("-i", dest="file", nargs=2, required=False, - help="upload local results; use `-i results.json overview.json`") + help="upload local results: `-i (coop_)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.add_argument("--getseed", required=False, action="store_true", @@ -1410,9 +1655,11 @@ def main(): # manual json upload: -i flag ############################# if filenames: # 2 files in list - if os.path.basename(filenames[0]) != "results.json" or os.path.basename(filenames[1]) != "overview.json": + if (os.path.basename(filenames[0]) != "results.json" \ + and os.path.basename(filenames[0]) != "coop_results.json") \ + or os.path.basename(filenames[1]) != "overview.json": print("Must use the format " \ - '\033[91m' + "-i path/to/results.json path/to/overview.json" + '\033[0m' + ". Exiting.") + '\033[91m' + "-i path/to/(coop_)results.json path/to/overview.json" + '\033[0m' + ". Exiting.") sys.exit(1) for filename in filenames: if not os.path.exists(filename): @@ -1422,7 +1669,7 @@ def main(): try: data = json.load(data_file) except ValueError: - print("Could not decode JSON object in results.json.") + print(f"Could not decode JSON object in {os.path.basename(filenames[0])}.") sys.exit(1) with open(filenames[1]) as data_file: try: @@ -1434,28 +1681,41 @@ def main(): # only upload unuploaded results auth = {'Authorization': f'Bearer {API_KEY}'} - resp = requests.get("https://stat.ink/api/v3/s3s/uuid-list", headers=auth) + resp_b = requests.get("https://stat.ink/api/v3/s3s/uuid-list", headers=auth) + resp_j = requests.get("https://stat.ink/api/v3/salmon/uuid-list", headers=auth) try: - statink_uploads = json.loads(resp.text) + statink_uploads = json.loads(resp_b.text) + statink_uploads.extend(json.loads(resp_j.text)) except: print(f"Encountered an error while checking recently-uploaded data. Is stat.ink down?") sys.exit(1) to_upload = [] - for battle in data: - if battle["data"]["vsHistoryDetail"] is not None: - full_id = utils.b64d(battle["data"]["vsHistoryDetail"]["id"]) - old_uuid = full_id[-36:] # not unique because nintendo hates us - new_uuid = str(uuid.uuid5(utils.S3S_NAMESPACE, full_id[-52:])) + for result in data: + try: # ink battle + if result["data"]["vsHistoryDetail"] is not None: + full_id = utils.b64d(result["data"]["vsHistoryDetail"]["id"]) + old_uuid = full_id[-36:] # not unique because nintendo hates us + new_uuid = str(uuid.uuid5(utils.S3S_NAMESPACE, full_id[-52:])) - if new_uuid in statink_uploads: - print("Skipping already-uploaded battle.") - continue - if old_uuid in statink_uploads: - if not utils.custom_key_exists("force_uploads", CONFIG_DATA): + if new_uuid in statink_uploads: print("Skipping already-uploaded battle.") continue - to_upload.append(battle) + if old_uuid in statink_uploads: + if not utils.custom_key_exists("force_uploads", CONFIG_DATA): + print("Skipping already-uploaded battle.") + continue + to_upload.append(result) + + except KeyError: # salmon run job + if result["data"]["coopHistoryDetail"] is not None: + full_id = utils.b64d(result["data"]["coopHistoryDetail"]["id"]) + the_uuid = str(uuid.uuid5(utils.SALMON_NAMESPACE, full_id[-52:])) + + if the_uuid in statink_uploads: + print("Skipping already-uploaded job.") + continue + to_upload.append(result) post_result(to_upload, False, blackout, test_run, overview_data=overview_file) # one or multiple; monitoring mode = False sys.exit(0) @@ -1468,7 +1728,10 @@ def main(): update_salmon_profile() if check_old: - check_if_missing(which, blackout, test_run) # monitoring mode hasn't begun yet + if which == "both": + prefetch_checks(printout=True) + skipprefetch = True + check_if_missing(which, blackout, test_run, skipprefetch) # monitoring mode hasn't begun yet print() if secs != -1: # monitoring mode