commit 83da4636ac56bc9873095e44ccc034cb424e428e Author: Remik1r3n Date: Thu Jan 23 18:52:09 2025 +0800 Initial restarted commit 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