forked from Kohaku/maimaiDX-Api
Initial restarted commit
This commit is contained in:
commit
83da4636ac
176
.gitignore
vendored
Normal file
176
.gitignore
vendored
Normal file
@ -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
|
110
API_AimeDB.py
Normal file
110
API_AimeDB.py
Normal file
@ -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))
|
170
API_TitleServer.py
Normal file
170
API_TitleServer.py
Normal file
@ -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
|
80
ActionDeleteMusicRecord.py
Normal file
80
ActionDeleteMusicRecord.py
Normal file
@ -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")
|
177
ActionLoginBonus.py
Normal file
177
ActionLoginBonus.py
Normal file
@ -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")
|
61
ActionUnlockVarious.py
Normal file
61
ActionUnlockVarious.py
Normal file
@ -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")
|
62
ChargeTicket.py
Normal file
62
ChargeTicket.py
Normal file
@ -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
|
69
DecryptHDD.py
Normal file
69
DecryptHDD.py
Normal file
@ -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()
|
16
GetPreview.py
Normal file
16
GetPreview.py
Normal file
@ -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))
|
36
HelperGetUserThing.py
Normal file
36
HelperGetUserThing.py
Normal file
@ -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
|
53
HelperLogInOut.py
Normal file
53
HelperLogInOut.py
Normal file
@ -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))
|
73
HelperUnlockThing.py
Normal file
73
HelperUnlockThing.py
Normal file
@ -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
|
146
HelperUploadUserPlayLog.py
Normal file
146
HelperUploadUserPlayLog.py
Normal file
@ -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
|
||||||
|
|
175
HelperUserAll.py
Normal file
175
HelperUserAll.py
Normal file
@ -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
|
8
Static_Settings.py
Normal file
8
Static_Settings.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
regionId = 22
|
||||||
|
regionName = "山东"
|
||||||
|
placeId = 3490
|
||||||
|
placeName = "赛博时空枣庄市中店"
|
||||||
|
clientId = "A63E01E9564"
|
||||||
|
|
||||||
|
# 日本精工,安全防漏
|
||||||
|
from Private_Static_Settings import *
|
97
UI.py
Normal file
97
UI.py
Normal file
@ -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())
|
242
loginBonus.json
Normal file
242
loginBonus.json
Normal file
@ -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倍チケット"
|
||||||
|
}
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user