From 83da4636ac56bc9873095e44ccc034cb424e428e Mon Sep 17 00:00:00 2001 From: Remik1r3n Date: Thu, 23 Jan 2025 18:52:09 +0800 Subject: [PATCH] Initial restarted commit --- .gitignore | 176 +++++++++++++++++++++++++++ API_AimeDB.py | 110 +++++++++++++++++ API_TitleServer.py | 170 ++++++++++++++++++++++++++ ActionDeleteMusicRecord.py | 80 ++++++++++++ ActionLoginBonus.py | 177 +++++++++++++++++++++++++++ ActionUnlockVarious.py | 61 ++++++++++ ChargeTicket.py | 62 ++++++++++ DecryptHDD.py | 69 +++++++++++ GetPreview.py | 16 +++ HelperGetUserThing.py | 36 ++++++ HelperLogInOut.py | 53 ++++++++ HelperUnlockThing.py | 73 +++++++++++ HelperUploadUserPlayLog.py | 146 ++++++++++++++++++++++ HelperUserAll.py | 175 +++++++++++++++++++++++++++ README.md | 2 + Static_Settings.py | 8 ++ UI.py | 97 +++++++++++++++ loginBonus.json | 242 +++++++++++++++++++++++++++++++++++++ 18 files changed, 1753 insertions(+) create mode 100644 .gitignore create mode 100644 API_AimeDB.py create mode 100644 API_TitleServer.py create mode 100644 ActionDeleteMusicRecord.py create mode 100644 ActionLoginBonus.py create mode 100644 ActionUnlockVarious.py create mode 100644 ChargeTicket.py create mode 100644 DecryptHDD.py create mode 100644 GetPreview.py create mode 100644 HelperGetUserThing.py create mode 100644 HelperLogInOut.py create mode 100644 HelperUnlockThing.py create mode 100644 HelperUploadUserPlayLog.py create mode 100644 HelperUserAll.py create mode 100644 README.md create mode 100644 Static_Settings.py create mode 100644 UI.py create mode 100644 loginBonus.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..321b44e --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Anti-leak +Private_Static_Settings.py + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# PyPI configuration file +.pypirc +Private_Static_Settings.py +Private_Static_Settings.py diff --git a/API_AimeDB.py b/API_AimeDB.py new file mode 100644 index 0000000..01635f8 --- /dev/null +++ b/API_AimeDB.py @@ -0,0 +1,110 @@ +import hashlib +import time +import requests +import json +import re + +# 计算 SHA256 +def compute_sha256(input_str): + """SHA256计算""" + return hashlib.sha256(input_str.encode('utf-8')).hexdigest().upper() + +# 生成时间戳 +def get_timestamp(): + """SEGA格式的 YYMMDDHHMMSS 时间戳(sb玩意)""" + return time.strftime("%y%m%d%H%M%S", time.localtime()) + +# 计算认证 key +def calculate_auth_key(time_stamp: str, chip_id: str, auth_key_param: str) -> str: + """计算 Key""" + return hashlib.sha256((chip_id + time_stamp + auth_key_param).encode("utf-8")).hexdigest().upper() + +def apiAimeDB(qr_code, chip_id, auth_key_param, game_id, api_url): + """AimeDB API 实现""" + # 生成一个时间戳 + time_stamp = get_timestamp() + + # 使用时间戳计算 key + auth_key = calculate_auth_key(time_stamp, chip_id, auth_key_param) + + # 构造请求数据 + payload = { + "chipID": chip_id, + "openGameID": game_id, + "key": auth_key, + "qrCode": qr_code, + "timestamp": time_stamp + } + + # 输出准备好的请求数据 + print("Payload:", json.dumps(payload, separators=(',', ':'))) + + # 发送 POST 请求 + headers = { + "Connection": "Keep-Alive", + "Host": api_url.split("//")[-1].split("/")[0], + "User-Agent": "WC_AIME_LIB", + "Content-Type": "application/json", + } + response = requests.post(api_url, data=json.dumps(payload, separators=(',', ':')), headers=headers) + + # 返回服务器的响应 + return response + + +def isSGWCFormat(input_string: str) -> bool: + '''简单检查二维码字符串是否符合格式''' + if ( + len(input_string) != 84 #长度 + or not input_string.startswith("SGWCMAID") #识别字 + or re.match("^[0-9A-F]+$", input_string[20:]) is None #有效字符 + ): + return False + else: + return True + + +def implAimeDB(qrcode_content_full:str) -> str: + ''' + Aime DB 的请求的参考实现。 + 提供完整 QRCode 内容,返回响应的字符串(Json格式) + ''' + CHIP_ID = "A63E-01E68606624" + AUTH_KEY_PARAM = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" + GAME_ID = "MAID" + API_URL = "http://ai.sys-allnet.cn/wc_aime/api/get_data" + + qr_code_final = qrcode_content_full[20:] + + # 发送请求 + response = apiAimeDB(qr_code_final, CHIP_ID, AUTH_KEY_PARAM, GAME_ID, API_URL) + + # 获得结果 + print("implAimeDB: StatusCode is ", response.status_code) + print("implAimeDB: Response Body is:", response.text) + return(response.text) + + +def implGetUID(qr_content:str) -> dict: + ''' + 包装后的 UID 扫码器实现。 + 此函数会返回 AimeDB 传回的 Json 转成 Python 字典的结果。 + 主要特点是添加了几个新的错误码(6000x)用来应对程序的错误。 + ''' + # 检查格式 + if not isSGWCFormat(qr_content): + return {'errorID': 60001} # 二维码内容明显无效 + + # 发送请求并处理响应 + try: + result_string = implAimeDB(qr_content) + result_dict = json.loads(result_string) + except: + return {'errorID': 60002} # 无法解码 Response 的内容 + + # 返回结果 + return result_dict + +if __name__ == "__main__": + userInputQR = input("QRCode: ") + print(implAimeDB(userInputQR)) diff --git a/API_TitleServer.py b/API_TitleServer.py new file mode 100644 index 0000000..d7eea97 --- /dev/null +++ b/API_TitleServer.py @@ -0,0 +1,170 @@ +# 舞萌DX 2024 +# 标题服务器通讯实现 + +import zlib +import hashlib +import requests +from loguru import logger +import random +import time + +from ctypes import c_int32 + +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad + +from Static_Settings import * + +# 舞萌DX 2024 +AesKey = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@" +AesIV = ";;KjR1C3hgB1ovXa" +ObfuscateParam = "BEs2D5vW" + +class WahlapServerBoomedError(Exception): + pass + +class Request500Error(Exception): + pass + +class aes_pkcs7(object): + def __init__(self, key: str, iv: str): + self.key = key.encode('utf-8') + self.iv = iv.encode('utf-8') + self.mode = AES.MODE_CBC + + def encrypt(self, content): + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + content_padding = self.pkcs7padding(content) + encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8')) + return encrypt_bytes + + def decrypt(self, content): + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + return cipher.decrypt(content) + + def pkcs7unpadding(self, text): + length = len(text) + unpadding = ord(text[length - 1]) + return text[0:length - unpadding] + + def pkcs7padding(self, text): + bs = 16 + length = len(text) + bytes_length = len(text.encode('utf-8')) + padding_size = length if (bytes_length == length) else bytes_length + padding = bs - padding_size % bs + padding_text = chr(padding) * padding + return text + padding_text + +def SDGBApiHash(api): + return hashlib.md5((api+"MaimaiChn"+ObfuscateParam).encode()).hexdigest() + +def apiSDGB(data:str, useApi, agentExtraData, maxRetries=5): + ''' + 舞萌DX 2024 API 通讯用函数 + :param data: 请求数据 + :param useApi: 使用的 API + :param agentExtraData: UA 附加信息,机台相关则为狗号(如A63E01E9564),用户相关则为 UID + :param maxRetry: 最大重试次数, 默认为 3 + ''' + + # 历史遗留代码有时候会传入 int,故先全部转 str + agentExtra = str(agentExtraData) + + # 编码好请求,准备发送 + aes = aes_pkcs7(AesKey,AesIV) + data = data + data_enc = aes.encrypt(data) + data_def = zlib.compress(data_enc) + requests.packages.urllib3.disable_warnings() + endpoint = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/" + + logger.debug("TitleServer Request Start: "+ str(useApi)+" , Data: "+str(data)) + + retries = 0 + while retries < maxRetries: + try: + # 发送请求 + responseRaw = requests.post(endpoint + SDGBApiHash(useApi), headers={ + "User-Agent": f"{SDGBApiHash(useApi)}#{agentExtra}", + "Content-Type": "application/json", + "Mai-Encoding": "1.40", + "Accept-Encoding": "", + "Charset": "UTF-8", + "Content-Encoding": "deflate", + "Expect": "100-continue" + }, data=data_def, verify=False) + logger.debug("TitleServer Request Sent.") + + logger.debug("TitleServer Response Code: " + str(responseRaw.status_code)) + # 如果是 404 或 500,直接抛出异常,不再继续重试 + match responseRaw.status_code: + case 200: + logger.debug("Request 200 OK!") + case 404: + logger.error(f"Request 404! ") + raise NotImplementedError + case 500: + logger.error(f"Request Failed! 500!!!! ") + raise Request500Error + case _: + logger.error(f"Request Failed! {responseRaw.status_code}") + raise NotImplementedError + responseContent = responseRaw.content + # 尝试解压请求 + try: + responseDecompressed = zlib.decompress(responseContent) + logger.debug("Successfully decompressed response.") + except zlib.error as e: + logger.warning(f"RAW Response: {responseContent}") + logger.warning(f"Wahlap Server Boomed! Will now retry.{e}") + retries += 1 + time.sleep(4) # 休眠4秒后重试 + continue + # 解压成功,解密请求并返回 + resultResponse = unpad(aes.decrypt(responseDecompressed), 16).decode() + logger.info("TitleServer Response OK!") + logger.debug("TitleServer Response: " + str(resultResponse)) + return resultResponse + + # 除了 404 和 500 之外的错误重试 + except Request500Error: + raise Request500Error("500,请求格式错误") + except NotImplementedError: + raise NotImplementedError("请求未知错误") + except Exception as e: + logger.warning(f"Request Failed! Will now retry.. {e}") + retries += 1 + time.sleep(4) + else: + # 重试次数用尽,WahlapServerBoomedError + raise WahlapServerBoomedError("重试多次仍然不能成功请求") + + +def calcSpecialNumber(): + '''使用 c_int32 实现的 SpecialNumber 算法''' + rng = random.SystemRandom() + num2 = rng.randint(1, 1037933) * 2069 + num2 += 0x400 + num2 = c_int32(num2).value + result = c_int32(0) + for _ in range(32): + result.value <<= 1 + result.value += num2 & 1 + num2 >>= 1 + return c_int32(result.value).value + + +def calcSpecialNumber2(): + '''实验性替代 SpecialNumber 算法''' + max = 1037933 + num2 = random.randint(1, max) * 2069 + + num2 += 1024 # specialnum + num3 = 0 + for i in range(0, 32): + num3 <<= 1 + num3 += num2 % 2 + num2 >>= 1 + + return num3 \ No newline at end of file diff --git a/ActionDeleteMusicRecord.py b/ActionDeleteMusicRecord.py new file mode 100644 index 0000000..55d5e00 --- /dev/null +++ b/ActionDeleteMusicRecord.py @@ -0,0 +1,80 @@ +# 删除成绩 + +import json +from loguru import logger + +from Static_Settings import * +from API_TitleServer import apiSDGB, calcSpecialNumber, WahlapServerBoomedError, Request500Error +from HelperLogInOut import apiLogin, apiLogout, generateTimestamp +from HelperGetUserThing import implGetUser_ +from HelperUploadUserPlayLog import apiUploadUserPlaylog +from HelperUserAll import generateFullUserAll + +def implDeleteMusicRecord(musicToBeDeleted: int, diffLevelId:int, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str: + ''' + 删除成绩的实现 + 需要在外部先登录并传入登录结果 + ''' + # 上传上去的歌曲记录 + musicDataToBeUploaded = ({ + "musicId": musicToBeDeleted, + "level": diffLevelId, + "playCount": 1, + "achievement": 0, + "comboStatus": 0, + "syncStatus": 0, + "deluxscoreMax": 0, + "scoreRank": 0, + "extNum1": 0 +}) + + # 取得 UserData + currentUserData = implGetUser_("Data", userId) + currentUserData2 = currentUserData['userData'] + + # 构建并上传一个游玩记录 + currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(userId, musicDataToBeUploaded, currentUserData2, currentLoginResult['loginId']) + logger.debug(f"上传 UserPlayLog 结果: {currentUploadUserPlaylogApiResult}") + + # 构建并上传 UserAll + retries = 0 + while retries < 3: + currentSpecialNumber = calcSpecialNumber() + currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber) + + currentUserAll['upsertUserAll']["userMusicDetailList"] = [musicDataToBeUploaded] + currentUserAll['upsertUserAll']['isNewMusicDetailList'] = "0" #0为编辑 即可删除掉成绩 + + data = json.dumps(currentUserAll) + try: + currentUserAllResult = json.loads(apiSDGB(data, "UpsertUserAllApi", userId)) + except Request500Error: + logger.warning("500 Error Triggered. Rebuilding data.") + retries += 1 + continue + except Exception: + raise WahlapServerBoomedError("邪门错误") + break + else: # 重试次数超过3次 + raise Request500Error("多次尝试后仍无法成功上传 UserAll") + + logger.info("删除成绩:结果:"+ str(currentUserAllResult)) + return currentUserAllResult + +if __name__ == "__main__": + userId = testUid + currentLoginTimestamp = generateTimestamp() + loginResult = apiLogin(currentLoginTimestamp, userId) + + musicId = 11548 #229 is guruguru wash + levelId = 3 #3 is MASTER + + if loginResult['returnCode'] != 1: + logger.info("登录失败") + exit() + try: + logger.info(implDeleteMusicRecord(musicId, levelId, userId, currentLoginTimestamp, loginResult)) + logger.info(apiLogout(currentLoginTimestamp, userId)) + except: + logger.info(apiLogout(currentLoginTimestamp, userId)) + logger.warning("Error") diff --git a/ActionLoginBonus.py b/ActionLoginBonus.py new file mode 100644 index 0000000..519a0a1 --- /dev/null +++ b/ActionLoginBonus.py @@ -0,0 +1,177 @@ +# ログインボーナス!やったね! +# セガ秘 内部使用のみ(トレードマーク) + +import json +from loguru import logger + +from Static_Settings import * +from API_TitleServer import apiSDGB, calcSpecialNumber, WahlapServerBoomedError, Request500Error +from HelperLogInOut import apiLogin, apiLogout, generateTimestamp +from HelperGetUserThing import implGetUser_ +from HelperUploadUserPlayLog import apiUploadUserPlaylog +from HelperUserAll import generateFullUserAll + +def implLoginBonus(userId: int, currentLoginTimestamp:int, currentLoginResult, bonusGenerateMode=2): + ''' + ログインボーナスデータをアップロードする + ''' + musicDataToBeUploaded = { + "musicId": 229, #ぐるぐるWASH + "level": 0, + "playCount": 2, + "achievement": 0, + "comboStatus": 0, + "syncStatus": 0, + "deluxscoreMax": 0, + "scoreRank": 0, + "extNum1": 0 +} + + # UserData を取得 + currentUserData = implGetUser_("Data", userId) + currentUserData2 = currentUserData['userData'] + + # UserPlayLog を構築してアップロード + currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(userId, musicDataToBeUploaded, currentUserData2, currentLoginResult['loginId']) + logger.debug(f"上传 UserPlayLog 结果: {currentUploadUserPlaylogApiResult}") + + # ログインボーナスリストを生成 + finalBonusList = generateLoginBonusList(userId,bonusGenerateMode) + if not finalBonusList: + return False + + # UserAllを構築してアップロード + retries = 0 + while retries < 3: + currentSpecialNumber = calcSpecialNumber() + currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber) + + currentUserAll['upsertUserAll']["userMusicDetailList"] = [musicDataToBeUploaded] + currentUserAll['upsertUserAll']['userLoginBonusList'] = finalBonusList + currentUserAll['upsertUserAll']['isNewMusicDetailList'] = "1" + currentUserAll['upsertUserAll']['isNewLoginBonusList'] = "0" * len(finalBonusList) + data = json.dumps(currentUserAll) + + try: + currentUserAllResult = json.loads(apiSDGB(data, "UpsertUserAllApi", userId)) + except Request500Error: + logger.warning("500 Error Triggered. Rebuilding data.") + retries += 1 + continue + except Exception: + raise WahlapServerBoomedError("邪门错误") + break + else: # 重试次数超过3次 + raise Request500Error("多次尝试后仍无法成功上传 UserAll") + + + ###### Debug 機能:ログインボーナスをもう一度取得(確認用) + apiLogout(currentLoginTimestamp, userId) + apiLogin(currentLoginTimestamp, userId) + data = json.dumps({ + "userId": int(userId), + "nextIndex": 0, + "maxCount": 2000 + }) + logger.debug(json.loads(apiSDGB(data, "GetUserLoginBonusApi", userId))) + ###### PRODUCTION 用には、上記のコードを削除してください + + return currentUserAllResult + +def generateLoginBonusList(userId, generateMode=1): + ''' + ログインボーナスリストを生成します。 + generateMode は、ログインボーナスを生成する方法を指定します。 + 1: 全部 MAX にする + 2: いま選択したボーナスのみ MAX にする(選択したボーナスはないの場合は False を返す) + ''' + + # ログインボーナスの MAX POINT は5の場合があります + # その全部のボーナスIDをこのリストに追加してください + Bonus5Id = [12, 29, 30, 38, 43, 604] + + # HDDから、ログインボーナスデータを読み込む + # アップデートがある場合、このファイルを更新する必要があります + # 必ず最新のデータを使用してください! + with open('loginBonus.json') as file: + cache = json.load(file) + loginBonusIdList = [item['id'] for item in cache] + logger.debug(f"ログインボーナスIDリスト: {loginBonusIdList}") + + # サーバーからUserのログインボーナスデータを取得 + data = json.dumps({ + "userId": int(userId), + "nextIndex": 0, + "maxCount": 2000 + }) + UserLoginBonusResponse = json.loads(apiSDGB(data, "GetUserLoginBonusApi", userId)) + UserLoginBonusList = UserLoginBonusResponse['userLoginBonusList'] + + # UserBonusList から bonusId を取得 + UserLoginBonusIdList = [item['bonusId'] for item in UserLoginBonusList] + + # 存在しないボーナス + NonExistingBonuses = list(set(loginBonusIdList) - set(UserLoginBonusIdList)) + logger.debug(f"存在しないボーナス: {NonExistingBonuses}") + + bonusList = [] + if generateMode == 1: # AllMax Mode + # 存在しているボーナスを追加 + for item in UserLoginBonusList: + if not item['isComplete']: + data = { + "bonusId": item['bonusId'], + "point": 4 if item['bonusId'] in Bonus5Id else 9, + "isCurrent": item['isCurrent'], + "isComplete": False + } + bonusList.append(data) + elif item['bonusId'] == 999: + data = { + "bonusId": 999, + "point": (item['point'] // 10) * 10 + 9, + "isCurrent": item['isCurrent'], + "isComplete": False + } + bonusList.append(data) + # 存在しないボーナスを追加 + for bonusId in NonExistingBonuses: + data = { + "bonusId": bonusId, + "point": 4 if bonusId in Bonus5Id else 9, + "isCurrent": False, + "isComplete": False + } + bonusList.append(data) + elif generateMode == 2: # OnlyCurrent Mode + for item in UserLoginBonusList: + if item['isCurrent'] and not item['isComplete']: + point = 4 if item['bonusId'] in Bonus5Id else 9 + data = { + "bonusId": item['bonusId'], + "point": point, + "isCurrent": True, + "isComplete": False + } + bonusList.append(data) + if len(bonusList) == 0: + logger.warning("このユーザーはログインボーナスを選択していませんから失敗") + return False + + logger.debug(f"ログインボーナスリスト: {bonusList}") + return bonusList + +if __name__ == "__main__": + userId = testUid + currentLoginTimestamp = generateTimestamp() + loginResult = apiLogin(currentLoginTimestamp, userId) + + if loginResult['returnCode'] != 1: + logger.info("登录失败") + exit() + try: + logger.info(implLoginBonus(userId, currentLoginTimestamp, loginResult)) + logger.info(apiLogout(currentLoginTimestamp, userId)) + except: + logger.info(apiLogout(currentLoginTimestamp, userId)) + logger.warning("Error") diff --git a/ActionUnlockVarious.py b/ActionUnlockVarious.py new file mode 100644 index 0000000..dc3dc45 --- /dev/null +++ b/ActionUnlockVarious.py @@ -0,0 +1,61 @@ +# 解锁一些东西的外部代码 + +from loguru import logger + +from Static_Settings import * +from HelperLogInOut import apiLogin, apiLogout, generateTimestamp +from HelperUnlockThing import implUnlockThing + +def implUnlockPartner(partnerToBeUnlocked: int, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str: + ''' + 解锁搭档 + ''' + userItemList = [ + { + "itemKind": 10, + "itemId": partnerToBeUnlocked, + "stock": 1, + "isValid": True + } + ], + unlockThingResult = implUnlockThing(userItemList, userId, currentLoginTimestamp, currentLoginResult) + return unlockThingResult + +def implUnlockMusic(musicToBeUnlocked: int, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str: + ''' + 解锁乐曲 + ''' + userItemList = [ + { + "itemKind": 5, + "itemId": musicToBeUnlocked, + "stock": 1, + "isValid": True + }, + { + "itemKind": 6, + "itemId": musicToBeUnlocked, + "stock": 1, + "isValid": True + }, + ], + unlockThingResult = implUnlockThing(userItemList, userId, currentLoginTimestamp, currentLoginResult) + return unlockThingResult + +if __name__ == "__main__": + userId = testUid + currentLoginTimestamp = generateTimestamp() + loginResult = apiLogin(currentLoginTimestamp, userId) + + wantToUnlockItemId = 1 + + if loginResult['returnCode'] != 1: + logger.info("登录失败") + exit() + try: + # change it + logger.info(implUnlockMusic(wantToUnlockItemId, userId, currentLoginTimestamp, loginResult)) + logger.info(apiLogout(currentLoginTimestamp, userId)) + except: + logger.info(apiLogout(currentLoginTimestamp, userId)) + logger.warning("Error") diff --git a/ChargeTicket.py b/ChargeTicket.py new file mode 100644 index 0000000..9f5e70d --- /dev/null +++ b/ChargeTicket.py @@ -0,0 +1,62 @@ +# 倍票相关 API 的实现 +import json +import pytz +from datetime import datetime, timedelta + +from Static_Settings import * +from API_TitleServer import apiSDGB +from HelperGetUserThing import apiGetUserData + +def apiQueryTicket(userId:int) -> str: + '''查询已有票的 API 请求器,返回 Json String。''' + # 构建 Payload + data = json.dumps({ + "userId": userId + }) + # 发送请求 + userdata_result = apiSDGB(data, "GetUserChargeApi", userId) + # 返回响应 + return userdata_result + +def apiBuyTicket(userId:int, ticketType:int, price:int, playerRating:int, playCount:int) -> str: + '''倍票购买 API 的请求器''' + # 构造请求数据 Payload + data = json.dumps({ + "userId": userId, + "userCharge": { + "chargeId": ticketType, + "stock": 1, + "purchaseDate": (datetime.now(pytz.timezone('Asia/Shanghai')) - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S.0"), + "validDate": (datetime.now(pytz.timezone('Asia/Shanghai')) - timedelta(hours=1) + timedelta(days=90)).replace(hour=4, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S") + }, + "userChargelog": { + "chargeId": ticketType, + "price": price, + "purchaseDate": (datetime.now(pytz.timezone('Asia/Shanghai')) - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S.0"), + "playcount": playCount, + "playerRating": playerRating, + "placeId": placeId, + "regionId": regionId, + "clientId": clientId + } + }) + # 发送请求,返回最终得到的 Json String 回执 + return apiSDGB(data, "UpsertUserChargelogApi", userId) + +def implBuyTicket(userId:int, ticketType:int) -> str: + ''' + 购买倍票 API 的参考实现。 + 需要事先登录. + 返回服务器响应的 Json string。 + ''' + # 先使用 GetUserData API 请求器,取得 rating 和 pc 数 + currentUserData = json.loads(apiGetUserData(userId)) + if currentUserData: + playerRating = currentUserData['userData']['playerRating'] + playCount = currentUserData['userData'].get('playCount', 0) + else: + return False + # 正式买票 + getTicketResponseStr = apiBuyTicket(userId, ticketType, ticketType-1, playerRating, playCount) + # 返回结果 + return getTicketResponseStr diff --git a/DecryptHDD.py b/DecryptHDD.py new file mode 100644 index 0000000..7a6be0d --- /dev/null +++ b/DecryptHDD.py @@ -0,0 +1,69 @@ +# 解密从 HDD 抓包得到的数据 +# 兼容 PRiSM 和 CN 2024 +# 仅用于分析 + +import base64 +import zlib +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad, pad + +# 密钥和 IV +# CN 2024 +aesKey2024 = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@" +aesIV2024 = ";;KjR1C3hgB1ovXa" + +# 国际服 PRiSM +aesKeyPrism = "A;mv5YUpHBK3YxTy5KB^[;5]C2AL50Bq" +aesIVPrism = "9FM:sd9xA91X14v]" + +class AESPKCS7: + def __init__(self, key: str, iv: str): + self.key = key.encode('utf-8') + self.iv = iv.encode('utf-8') + self.mode = AES.MODE_CBC + + def encrypt(self, content: bytes) -> bytes: + cipher = AES.new(self.key, self.mode, self.iv) + content_padded = pad(content, AES.block_size) + encrypted_bytes = cipher.encrypt(content_padded) + return encrypted_bytes + + def decrypt(self, encrypted_content: bytes) -> str: + cipher = AES.new(self.key, self.mode, self.iv) + decrypted_padded = cipher.decrypt(encrypted_content) + decrypted = unpad(decrypted_padded, AES.block_size) + return decrypted + +def main_sdga(): + # 填入你的想解密的数据的 base64 编码 + base64_encoded_data = "KSGm2qo7qVHz1wrK15PckYC5/kLjKcTtEXOgHeHt1Xn6DPdo3pltoPLADHpe8+Wq" + + aes = AESPKCS7(aesKeyPrism, aesIVPrism) + + # 首先解码 base64 + decodedData = base64.b64decode(base64_encoded_data) + # 然后解密数据,PRiSM 是先压缩再加密(which is 正确做法) + decryptedData = aes.decrypt(decodedData) + # 解压数据 + decompressedData = zlib.decompress(decryptedData) + + print(str(decompressedData)) + +def main_sdgb(): + # 填入你的想解密的数据的 base64 编码 + base64_encoded_data = "eJyrTVvpuGwCR32OdodwtVXZ7/Ofmfhin7k/K61q3XNoad1rAPGwECU=" + + aes = AESPKCS7(aesKey2024, aesIV2024) + + # 首先解码 base64 + decodedData = base64.b64decode(base64_encoded_data) + # 然后解压数据,CN 2024 是加密后再压缩(纯傻逼 + decompressedData = zlib.decompress(decodedData) + # 最后解密数据 + decryptedData = aes.decrypt(decompressedData) + + print(str(decryptedData)) + + +if __name__ == "__main__": + main_sdga() diff --git a/GetPreview.py b/GetPreview.py new file mode 100644 index 0000000..f8cff1e --- /dev/null +++ b/GetPreview.py @@ -0,0 +1,16 @@ +# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取 + +import json +from API_TitleServer import apiSDGB + +def apiGetUserPreview(userId) -> str: + data = json.dumps({ + "userId": int(userId) + }) + preview_result = apiSDGB(data, "GetUserPreviewApi", userId) + return preview_result + +# CLI 示例 +if __name__ == "__main__": + userId = input("请输入用户 ID:") + print(apiGetUserPreview(userId)) diff --git a/HelperGetUserThing.py b/HelperGetUserThing.py new file mode 100644 index 0000000..9b6d793 --- /dev/null +++ b/HelperGetUserThing.py @@ -0,0 +1,36 @@ +# 获取用户数据的 API 实现 +from loguru import logger +import json +from API_TitleServer import apiSDGB + +def apiGetUserData(userId:int) -> str: + '''已弃用,将逐步淘汰''' + logger.warning("apiGetUserData 已弃用,将逐步淘汰。") + # 构建 Payload + data = json.dumps({ + "userId": userId + }) + # 发送请求 + userdata_result = apiSDGB(data, "GetUserDataApi", userId) + # 返回响应 + return userdata_result + +def apiGetUserThing(userId:int, thing:str) -> str: + '''获取用户数据的 API 请求器,返回 Json String''' + # 构建 Payload + data = json.dumps({ + "userId": userId + }) + # 发送请求 + userthing_result = apiSDGB(data, "GetUser" + thing + "Api", userId) + # 返回响应 + return userthing_result + +def implGetUser_(thing:str, userId:int) -> dict: + '''获取用户数据的 API 实现,返回 Dict''' + # 获取 Json String + userthing_result = apiGetUserThing(userId, thing) + # 转换为 Dict + userthing_dict = json.loads(userthing_result) + # 返回 Dict + return userthing_dict \ No newline at end of file diff --git a/HelperLogInOut.py b/HelperLogInOut.py new file mode 100644 index 0000000..885d354 --- /dev/null +++ b/HelperLogInOut.py @@ -0,0 +1,53 @@ +# 登录·登出实现 +# 一般作为模块使用,但也可以作为 CLI 程序运行以强制登出账号。 + +import json +import time +from loguru import logger + +from Static_Settings import * +from API_TitleServer import apiSDGB + +def apiLogin(timestamp:int, userId:int) -> dict: + '''登录,返回服务器给的 Json 的 dict''' + data = json.dumps({ + "userId": userId, + "accessCode": "", + "regionId": regionId, + "placeId": placeId, + "clientId": clientId, + "dateTime": timestamp, + "isContinue": False, + "genericFlag": 0, + }) + login_result = json.loads(apiSDGB(data, "UserLoginApi", userId)) + logger.info("登录:结果:"+ str(login_result)) + return login_result + +def apiLogout(timestamp:int, userId:int) -> dict: + '''登出,返回 Json dict''' + data = json.dumps({ + "userId": userId, + "accessCode": "", + "regionId": regionId, + "placeId": placeId, + "clientId": clientId, + "dateTime": timestamp, + "type": 1 + }) + logout_result = json.loads(apiSDGB(data, "UserLogoutApi", userId)) + logger.info("登出:结果:"+ str(logout_result)) + return logout_result + + +def generateTimestamp() -> int: + '''生成时间戳''' + timestamp = int(time.time()) - 60 + logger.info(f"生成时间戳: {timestamp}") + return timestamp + +if __name__ == "__main__": + print("强制登出 CLI") + uid = testUid + timestamp = input("Timestamp: ") + apiLogout(int(timestamp), int(uid)) diff --git a/HelperUnlockThing.py b/HelperUnlockThing.py new file mode 100644 index 0000000..65912ac --- /dev/null +++ b/HelperUnlockThing.py @@ -0,0 +1,73 @@ +# 解锁东西的一个通用的助手,不可独立使用 + +import json +from loguru import logger + +from Static_Settings import * +from API_TitleServer import apiSDGB, calcSpecialNumber, WahlapServerBoomedError, Request500Error +from HelperGetUserThing import implGetUser_ +from HelperUploadUserPlayLog import apiUploadUserPlaylog +from HelperUserAll import generateFullUserAll + +def implUnlockThing(newUserItemList:list, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str: + ''' + 解锁东西的实现 + Note: itemKind 如下 + PLATE = 1 # 姓名框 + TITLE = 2 # 称号 + ICON = 3 # 头像 + PRESENT = 4 + MUSIC = 5 # 乐曲 + MUSIC_MASTER = 6 + MUSIC_RE_MASTER = 7 + MUSIC_STRONG = 8 + CHARACTER = 9 # 旅行伙伴 + PARTNER = 10 # 搭档 + FRAME = 11 # 背景板 + TICKET = 12 # 功能票 + ''' + # 上传上去的歌曲记录 + musicDataToBeUploaded = ({ + "musicId": 229, #洗衣机 + "level": 0, + "playCount": 2, + "achievement": 0, + "comboStatus": 0, + "syncStatus": 0, + "deluxscoreMax": 0, + "scoreRank": 0, + "extNum1": 0 + }) + + # UserData を取得 + currentUserData = implGetUser_("Data", userId) + currentUserData2 = currentUserData['userData'] + + # UserPlayLog を構築してアップロード + currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(userId, musicDataToBeUploaded, currentUserData2, currentLoginResult['loginId']) + logger.debug(f"上传 UserPlayLog 结果: {currentUploadUserPlaylogApiResult}") + + # UserAllを構築してアップロード + retries = 0 + while retries < 3: + currentSpecialNumber = calcSpecialNumber() + currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber) + + currentUserAll['upsertUserAll']["userMusicDetailList"] = [musicDataToBeUploaded] + currentUserAll['upsertUserAll']['isNewMusicDetailList'] = "1" # Insert mode(Not overriding) + currentUserAll['upsertUserAll']['userItemList'] = newUserItemList + data = json.dumps(currentUserAll) + try: + currentUserAllResult = json.loads(apiSDGB(data, "UpsertUserAllApi", userId)) + except Request500Error: + logger.warning("500 Error Triggered. Rebuilding data.") + retries += 1 + continue + except Exception: + raise WahlapServerBoomedError("邪门错误") + break + else: # 重试次数超过3次 + raise Request500Error("多次尝试后仍无法成功上传 UserAll") + + logger.info("解锁东西:结果:"+ str(currentUserAllResult)) + return currentUserAllResult diff --git a/HelperUploadUserPlayLog.py b/HelperUploadUserPlayLog.py new file mode 100644 index 0000000..bd9d658 --- /dev/null +++ b/HelperUploadUserPlayLog.py @@ -0,0 +1,146 @@ +# 上传一个占位用的游玩记录的 API 实现 + +import json +import pytz +import time +import random +from datetime import datetime +from loguru import logger + +from API_TitleServer import apiSDGB +from Static_Settings import * + +def apiUploadUserPlaylog(userId:int, musicDataToBeUploaded, currentUserData2, loginId:int) -> str: + '''返回 Json String。''' + + # 暂存,优化可读性(迫真) + musicId = musicDataToBeUploaded['musicId'] + level = musicDataToBeUploaded['level'] + #playCount = musicDataToBeUploaded['playCount'] + achievement = musicDataToBeUploaded['achievement'] + #comboStatus = musicDataToBeUploaded['comboStatus'] + #syncStatus = musicDataToBeUploaded['syncStatus'] + deluxscoreMax = musicDataToBeUploaded['deluxscoreMax'] + scoreRank = musicDataToBeUploaded['scoreRank'] + #extNum1 = musicDataToBeUploaded['extNum1'] + + # 构建一个 PlayLog + data = json.dumps({ + "userId": int(userId), + "userPlaylog": { + "userId": 0, + "orderId": 0, + "playlogId": loginId, + "version": 1041000, + "placeId": placeId, + "placeName": placeName, + "loginDate": int(time.time()), #似乎和登录timestamp不同,暂时不作更改 + "playDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d'), + "userPlayDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0', + "type": 0, + "musicId": int(musicId), + "level": int(level), + "trackNo": 1, + "vsMode": 0, + "vsUserName": "", + "vsStatus": 0, + "vsUserRating": 0, + "vsUserAchievement": 0, + "vsUserGradeRank": 0, + "vsRank": 0, + "playerNum": 1, + "playedUserId1": 0, + "playedUserName1": "", + "playedMusicLevel1": 0, + "playedUserId2": 0, + "playedUserName2": "", + "playedMusicLevel2": 0, + "playedUserId3": 0, + "playedUserName3": "", + "playedMusicLevel3": 0, + "characterId1": currentUserData2['charaSlot'][0], + "characterLevel1": random.randint(1000,6500), + "characterAwakening1": 5, + "characterId2": currentUserData2['charaSlot'][1], + "characterLevel2": random.randint(1000,6500), + "characterAwakening2": 5, + "characterId3": currentUserData2['charaSlot'][2], + "characterLevel3": random.randint(1000,6500), + "characterAwakening3": 5, + "characterId4": currentUserData2['charaSlot'][3], + "characterLevel4": random.randint(1000,6500), + "characterAwakening4": 5, + "characterId5": currentUserData2['charaSlot'][4], + "characterLevel5": random.randint(1000,6500), + "characterAwakening5": 5, + "achievement": int(achievement), + "deluxscore": int(deluxscoreMax), + "scoreRank": int(scoreRank), + "maxCombo": 0, + "totalCombo": random.randint(700,900), + "maxSync": 0, + "totalSync": 0, + "tapCriticalPerfect": 0, + "tapPerfect": 0, + "tapGreat": 0, + "tapGood": 0, + "tapMiss": random.randint(1,10), + "holdCriticalPerfect": 0, + "holdPerfect": 0, + "holdGreat": 0, + "holdGood": 0, + "holdMiss": random.randint(1,15), + "slideCriticalPerfect": 0, + "slidePerfect": 0, + "slideGreat": 0, + "slideGood": 0, + "slideMiss": random.randint(1,15), + "touchCriticalPerfect": 0, + "touchPerfect": 0, + "touchGreat": 0, + "touchGood": 0, + "touchMiss": random.randint(1,15), + "breakCriticalPerfect": 0, + "breakPerfect": 0, + "breakGreat": 0, + "breakGood": 0, + "breakMiss": random.randint(1,15), + "isTap": True, + "isHold": True, + "isSlide": True, + "isTouch": True, + "isBreak": True, + "isCriticalDisp": True, + "isFastLateDisp": True, + "fastCount": 0, + "lateCount": 0, + "isAchieveNewRecord": True, + "isDeluxscoreNewRecord": True, + "comboStatus": 0, + "syncStatus": 0, + "isClear": False, + 'beforeRating': currentUserData2['playerRating'], + 'afterRating': currentUserData2['playerRating'], + "beforeGrade": 0, + "afterGrade": 0, + "afterGradeRank": 1, + 'beforeDeluxRating': currentUserData2['playerRating'], + 'afterDeluxRating': currentUserData2['playerRating'], + "isPlayTutorial": False, + "isEventMode": False, + "isFreedomMode": False, + "playMode": 0, + "isNewFree": False, + "trialPlayAchievement": -1, + "extNum1": 0, + "extNum2": 0, + "extNum4": 3020, + "extBool1": False + } + }) + # 发送请求 + result = apiSDGB(data, "UploadUserPlaylogApi", userId) + logger.info("上传游玩记录:结果:"+ str(result)) + # 返回响应 + return result + diff --git a/HelperUserAll.py b/HelperUserAll.py new file mode 100644 index 0000000..77d18c7 --- /dev/null +++ b/HelperUserAll.py @@ -0,0 +1,175 @@ +# UserAll 有关的一些辅助函数 + +import pytz +from datetime import datetime +from Static_Settings import * +from HelperGetUserThing import implGetUser_ + +def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber): + '''从服务器取得必要的数据并构建一个比较完整的 UserAll''' + + # 先构建一个基础 UserAll + currentUserAll = generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber) + + # 然后从服务器取得必要的数据 + currentUserExtend = implGetUser_("Extend", userId) + currentUserOption = implGetUser_("Option", userId) + currentUserRating = implGetUser_("Rating", userId) + currentUserActivity = implGetUser_("Activity", userId) + currentUserCharge = implGetUser_("Charge", userId) + + # 把这些数据都追加进去 + currentUserAll['upsertUserAll']['userExtend'] = [currentUserExtend['userExtend']] + currentUserAll['upsertUserAll']['userOption'] = [currentUserOption['userOption']] + currentUserAll['upsertUserAll']['userRatingList'] = [currentUserRating['userRating']] + currentUserAll['upsertUserAll']['userActivityList'] = [currentUserActivity['userActivity']] + currentUserAll['upsertUserAll']['userChargeList'] = currentUserCharge['userChargeList'] + + # 完事 + return currentUserAll + + +def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber): + '''构建一个非常基础的 UserAll 数据,必须手动填充一些数据''' + + data = { + "userId": userId, + "playlogId": currentLoginResult['loginId'], + "isEventMode": False, + "isFreePlay": False, + "upsertUserAll": { + "userData": [ + { + "accessCode": "", + "userName": currentUserData2['userName'], + "isNetMember": 1, + "iconId": currentUserData2['iconId'], + "plateId": currentUserData2['plateId'], + "titleId": currentUserData2['titleId'], + "partnerId": currentUserData2['partnerId'], + "frameId": currentUserData2['frameId'], + "selectMapId": currentUserData2['selectMapId'], + "totalAwake": currentUserData2['totalAwake'], + "gradeRating": currentUserData2['gradeRating'], + "musicRating": currentUserData2['musicRating'], + "playerRating": currentUserData2['playerRating'], + "highestRating": currentUserData2['highestRating'], + "gradeRank": currentUserData2['gradeRank'], + "classRank": currentUserData2['classRank'], + "courseRank": currentUserData2['courseRank'], + "charaSlot": currentUserData2['charaSlot'], + "charaLockSlot": currentUserData2['charaLockSlot'], + "contentBit": currentUserData2['contentBit'], + "playCount": currentUserData2['playCount'], + "currentPlayCount": currentUserData2['currentPlayCount'], + "renameCredit": 0, + "mapStock": currentUserData2['mapStock'], + "eventWatchedDate": currentUserData2['eventWatchedDate'], + "lastGameId": "SDGB", + "lastRomVersion": currentUserData2['lastRomVersion'], + "lastDataVersion": currentUserData2['lastDataVersion'], + "lastLoginDate": currentLoginResult['lastLoginDate'], # sb + "lastPlayDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0', + "lastPlayCredit": 1, + "lastPlayMode": 0, + "lastPlaceId": placeId, + "lastPlaceName": placeName, + "lastAllNetId": 0, + "lastRegionId": regionId, + "lastRegionName": regionName, + "lastClientId": clientId, + "lastCountryCode": "CHN", + "lastSelectEMoney": 0, + "lastSelectTicket": 0, + "lastSelectCourse": currentUserData2['lastSelectCourse'], + "lastCountCourse": 0, + "firstGameId": "SDGB", + "firstRomVersion": currentUserData2['firstRomVersion'], + "firstDataVersion": currentUserData2['firstDataVersion'], + "firstPlayDate": currentUserData2['firstPlayDate'], + "compatibleCmVersion": currentUserData2['compatibleCmVersion'], + "dailyBonusDate": currentUserData2['dailyBonusDate'], + "dailyCourseBonusDate": currentUserData2['dailyCourseBonusDate'], + "lastPairLoginDate": currentUserData2['lastPairLoginDate'], + "lastTrialPlayDate": currentUserData2['lastTrialPlayDate'], + "playVsCount": 0, + "playSyncCount": 0, + "winCount": 0, + "helpCount": 0, + "comboCount": 0, + "totalDeluxscore": currentUserData2['totalDeluxscore'], + "totalBasicDeluxscore": currentUserData2['totalBasicDeluxscore'], + "totalAdvancedDeluxscore": currentUserData2['totalAdvancedDeluxscore'], + "totalExpertDeluxscore": currentUserData2['totalExpertDeluxscore'], + "totalMasterDeluxscore": currentUserData2['totalMasterDeluxscore'], + "totalReMasterDeluxscore": currentUserData2['totalReMasterDeluxscore'], + "totalSync": currentUserData2['totalSync'], + "totalBasicSync": currentUserData2['totalBasicSync'], + "totalAdvancedSync": currentUserData2['totalAdvancedSync'], + "totalExpertSync": currentUserData2['totalExpertSync'], + "totalMasterSync": currentUserData2['totalMasterSync'], + "totalReMasterSync": currentUserData2['totalReMasterSync'], + "totalAchievement": currentUserData2['totalAchievement'], + "totalBasicAchievement": currentUserData2['totalBasicAchievement'], + "totalAdvancedAchievement": currentUserData2['totalAdvancedAchievement'], + "totalExpertAchievement": currentUserData2['totalExpertAchievement'], + "totalMasterAchievement": currentUserData2['totalMasterAchievement'], + "totalReMasterAchievement": currentUserData2['totalReMasterAchievement'], + "playerOldRating": currentUserData2['playerOldRating'], + "playerNewRating": currentUserData2['playerNewRating'], + "banState": 0, + "dateTime": currentLoginTimestamp + } + ], + "userExtend": [], #需要填上 + "userOption": [], #需要填上 + "userGhost": [], + "userCharacterList": [], + "userMapList": [], + "userLoginBonusList": [], + "userRatingList": [], #需要填上 + "userItemList": [], #可选,但经常要填上 + "userMusicDetailList": [],#需要填上 + "userCourseList": [], + "userFriendSeasonRankingList": [], + "userChargeList": [], #需要填上 + "userFavoriteList": [], + "userActivityList": [], #需要填上 + "userGamePlaylogList": [ + { + "playlogId": currentLoginResult['loginId'], + "version": "1.41.00", + "playDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0', + "playMode": 0, + "useTicketId": -1, + "playCredit": 1, + "playTrack": 1, + "clientId": clientId, + "isPlayTutorial": False, + "isEventMode": False, + "isNewFree": False, + "playCount": currentUserData2['playCount'], + "playSpecial": currentSpecialNumber, + "playOtherUserId": 0 + } + ], + "user2pPlaylog": { + "userId1": 0, + "userId2": 0, + "userName1": "", + "userName2": "", + "regionId": 0, + "placeId": 0, + "user2pPlaylogDetailList": [] + }, + "isNewCharacterList": "", + "isNewMapList": "", + "isNewLoginBonusList": "", + "isNewItemList": "", + "isNewMusicDetailList": "", #可选但经常要填上 + "isNewCourseList": "0", + "isNewFavoriteList": "", + "isNewFriendSeasonRankingList": "" + } + } + return data diff --git a/README.md b/README.md new file mode 100644 index 0000000..56587da --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# genshin-impact +Genshin Impact Helpers and Tools diff --git a/Static_Settings.py b/Static_Settings.py new file mode 100644 index 0000000..2011eff --- /dev/null +++ b/Static_Settings.py @@ -0,0 +1,8 @@ +regionId = 22 +regionName = "山东" +placeId = 3490 +placeName = "赛博时空枣庄市中店" +clientId = "A63E01E9564" + +# 日本精工,安全防漏 +from Private_Static_Settings import * diff --git a/UI.py b/UI.py new file mode 100644 index 0000000..708a54f --- /dev/null +++ b/UI.py @@ -0,0 +1,97 @@ +import sys +import json + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QLineEdit, QTextEdit, QPushButton, QLabel, QHBoxLayout +) +from PyQt6.QtCore import Qt + +from API_TitleServer import * + +def sendRequest(requestText:str, apiNameText:str, uid:int) -> str: + try: + data = json.loads(requestText) + data = json.dumps(data) + except: + return "给出的输入不是有效的 JSON" + + result = apiSDGB(data, apiNameText, uid) + return result + + +class ApiTester(QMainWindow): + def __init__(self): + super().__init__() + + # 主窗口设定 + self.setWindowTitle("舞萌DX 2024 API 测试器") + self.resize(640, 400) + # 布局 + mainWidget = QWidget() + self.setCentralWidget(mainWidget) + MainLayout = QVBoxLayout(mainWidget) + + # 目标 API 输入框布局 + TargetAPILayout = QHBoxLayout() + # API 输入框 + self.TargetAPIInputBox = QLineEdit() + self.TargetAPIInputBox.setPlaceholderText("指定 API") + TargetAPILayout.addWidget(self.TargetAPIInputBox) + # API 后缀标签 + TargetAPILabel = QLabel("MaimaiChn") + TargetAPILayout.addWidget(TargetAPILabel) + # 添加到主布局 + MainLayout.addLayout(TargetAPILayout) + + # UA额外信息输入框 + self.AgentExtraInputBox = QLineEdit() + self.AgentExtraInputBox.setPlaceholderText("指定附加信息(UID或狗号)") + MainLayout.addWidget(self.AgentExtraInputBox) + + # 请求输入框 + self.RequestInputBox = QTextEdit() + self.RequestInputBox.setPlaceholderText("此处填入请求") + MainLayout.addWidget(self.RequestInputBox) + # 发送按钮 + SendRequestButton = QPushButton("发送!") + SendRequestButton.clicked.connect(self.prepareRequest) + MainLayout.addWidget(SendRequestButton) + # 响应输出框 + self.ResponseTextBox = QTextEdit() + self.ResponseTextBox.setPlaceholderText("此处显示输出") + self.ResponseTextBox.setReadOnly(True) + MainLayout.addWidget(self.ResponseTextBox) + + + + # 布局设定 + MainLayout.setContentsMargins(5, 5, 5, 5) + MainLayout.setSpacing(5) + MainLayout.setAlignment(Qt.AlignmentFlag.AlignTop) + + + def prepareRequest(self): + # 发送请求用 + try: + RequestDataString = self.RequestInputBox.toPlainText() + TargetAPIString = self.TargetAPIInputBox.text() + AgentExtraString = int(self.AgentExtraInputBox.text()) + except: + self.ResponseTextBox.setPlainText("输入无效") + return + + Result = sendRequest(RequestDataString, TargetAPIString, AgentExtraString) + + # 显示出输出 + self.ResponseTextBox.setPlainText(Result) + +if __name__ == "__main__": + app = QApplication(sys.argv) + # Set proper style for each OS + #if sys.platform == "win32": + # app.setStyle("windowsvista") + #else: + # app.setStyle("Fusion") + window = ApiTester() + window.show() + sys.exit(app.exec()) diff --git a/loginBonus.json b/loginBonus.json new file mode 100644 index 0000000..6408463 --- /dev/null +++ b/loginBonus.json @@ -0,0 +1,242 @@ +[ + { + "id": 38, + "name": "パートナー:ずんだもん" + }, + { + "id": 39, + "name": "パートナー:乙姫(ばでぃーず)" + }, + { + "id": 40, + "name": "パートナー:らいむっくま&れもんっくま(ばでぃーず)" + }, + { + "id": 34, + "name": "パートナー:黒姫" + }, + { + "id": 24, + "name": "パートナー:ラズ(ふぇすてぃばる)" + }, + { + "id": 25, + "name": "パートナー:シフォン(ふぇすてぃばる)" + }, + { + "id": 26, + "name": "パートナー:ソルト(ふぇすてぃばる)" + }, + { + "id": 19, + "name": "パートナー:ちびみるく" + }, + { + "id": 20, + "name": "パートナー:百合咲ミカ" + }, + { + "id": 8, + "name": "パートナー:しゃま(ゆにばーす)" + }, + { + "id": 9, + "name": "パートナー:みるく(ゆにばーす)" + }, + { + "id": 7, + "name": "パートナー:乙姫(すぷらっしゅ)" + }, + { + "id": 1, + "name": "パートナー:乙姫" + }, + { + "id": 2, + "name": "パートナー:ラズ" + }, + { + "id": 3, + "name": "パートナー:シフォン" + }, + { + "id": 4, + "name": "パートナー:ソルト" + }, + { + "id": 5, + "name": "パートナー:しゃま" + }, + { + "id": 6, + "name": "パートナー:みるく" + }, + { + "id": 605, + "name": "でらっくす譜面:oboro" + }, + { + "id": 606, + "name": "でらっくす譜面:ナミダと流星" + }, + { + "id": 607, + "name": "スタンダード譜面:渦状銀河のシンフォニエッタ" + }, + { + "id": 508, + "name": "でらっくす譜面:LatentKingdom" + }, + { + "id": 41, + "name": "でらっくす譜面:初音ミクの消失" + }, + { + "id": 42, + "name": "でらっくす譜面:色は匂へど散りぬるを" + }, + { + "id": 601, + "name": "でらっくす譜面:BULKUP(GAMEEXCLUSIVEEDIT)" + }, + { + "id": 602, + "name": "でらっくす譜面:MonochromeRainbow" + }, + { + "id": 603, + "name": "でらっくす譜面:Selector" + }, + { + "id": 35, + "name": "でらっくす譜面:深海少女" + }, + { + "id": 36, + "name": "でらっくす譜面:ナイト・オブ・ナイツ" + }, + { + "id": 27, + "name": "でらっくす譜面:M.S.S.Planet" + }, + { + "id": 28, + "name": "でらっくす譜面:響縁" + }, + { + "id": 501, + "name": "スタンダード譜面:Halcyon" + }, + { + "id": 502, + "name": "スタンダード譜面:サンバランド" + }, + { + "id": 503, + "name": "でらっくす譜面:StarlightDisco" + }, + { + "id": 504, + "name": "でらっくす譜面:火炎地獄" + }, + { + "id": 505, + "name": "スタンダード譜面:VIIIbitExplorer" + }, + { + "id": 506, + "name": "でらっくす譜面:Maxi" + }, + { + "id": 507, + "name": "でらっくす譜面:ケロ⑨destiny" + }, + { + "id": 21, + "name": "でらっくす譜面:セツナトリップ" + }, + { + "id": 22, + "name": "でらっくす譜面:Grip&Breakdown!!" + }, + { + "id": 17, + "name": "でらっくす譜面:ゴーストルール" + }, + { + "id": 18, + "name": "でらっくす譜面:tabootearsyouup" + }, + { + "id": 43, + "name": "アイコン:BUDDiES" + }, + { + "id": 604, + "name": "アイコン:FESTiVALラズ&シフォン&ソルト" + }, + { + "id": 29, + "name": "アイコン:FESTiVAL" + }, + { + "id": 30, + "name": "アイコン:Lia=Fail" + }, + { + "id": 12, + "name": "アイコン:UNiVERSE" + }, + { + "id": 14, + "name": "ネームプレート:はっぴー(ゆにばーす)" + }, + { + "id": 44, + "name": "フレーム:mystiqueasiris" + }, + { + "id": 45, + "name": "フレーム:VeRForTeαRtE:VEiN" + }, + { + "id": 37, + "name": "フレーム:Tricolor⁂circuS" + }, + { + "id": 31, + "name": "フレーム:HeavenlyBlast" + }, + { + "id": 32, + "name": "フレーム:sølips" + }, + { + "id": 33, + "name": "フレーム:RainbowRushStory" + }, + { + "id": 23, + "name": "フレーム:ふたりでばかんすにゃ♪" + }, + { + "id": 15, + "name": "フレーム:ここからはじまるプロローグ。" + }, + { + "id": 16, + "name": "フレーム:モ゜ルモ゜ル" + }, + { + "id": 10, + "name": "フレーム:黒姫" + }, + { + "id": 11, + "name": "フレーム:百合咲ミカ" + }, + { + "id": 999, + "name": "ちほー進行1.5倍チケット" + } + ] \ No newline at end of file