refactor: update to sdgb 1.55

This commit is contained in:
2026-07-07 13:38:06 +08:00
parent 8da425391a
commit 17c419dca0
18 changed files with 4678 additions and 4360 deletions

24
src/sdgb/.settings.py Normal file
View File

@@ -0,0 +1,24 @@
# This file contains the config. No function inside.
# DO NOT share your env to others.
userId = ...
qrCode = ...
musicData = {
"musicId": 417,
"level": 3,
"playCount": 1,
"achievement": 1010000,
"comboStatus": 4,
"syncStatus": 4,
"deluxscoreMax": 2277,
"scoreRank": 13,
"extNum1": 0,
}
regionId = 1
regionName = "北京"
placeId = 1403
placeName = "插电师北京王府井银泰店"
clientId = "A63E01C2805"
KeychipID = "A63E-01C28055905"

View File

@@ -0,0 +1,29 @@
import asyncio
import httpx
from sdgb.sdgb import MaimaiClient
from sdgb.chime import qr_api
from sdgb.settings import clientId, qrCode
maimai = MaimaiClient()
userId = qr_api(qrCode)["userID"]
token = qr_api(qrCode)["token"]
async def run_workflow(maimai: MaimaiClient):
async with httpx.AsyncClient(verify=False) as client:
data = {
"userId": userId,
"segaIdAuthKey": "",
"token": token,
"clientId": clientId,
}
result = await maimai.call_api(client, "GetUserPreviewApi", data, userId)
print(result)
# 执行入口
if __name__ == "__main__":
asyncio.run(run_workflow(maimai))

66
src/sdgb/PowerOn.py Normal file
View File

@@ -0,0 +1,66 @@
from urllib.parse import unquote
import httpx
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def enc(key, iv, data):
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(data)
return encrypted
def dec(key, iv, data):
de_cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = de_cipher.decrypt(data)
return decrypted
def hello():
key = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
# key = bytes([ 45, 97, 53, 55, 85, 88, 52, 121, 57, 47, 104, 40, 73, 109, 65, 81 ])
iv = bytes.fromhex("00000000000000000000000000000000")
ua = "SDGB;Windows/Lite"
# ua = 'SDHJ;Windows/Lite'
# 构建 payload
content = bytes([0] * 16) + b"title_id=SDGB&title_ver=1.52&client_id=A63E01E6149"
print(f"Content: {content}")
header = bytes.fromhex("00000000000000000000000000000000")
bytes_data = pad(header + content, 16)
encrypted = enc(key, iv, bytes_data)
# --- HTTPX 修改部分 ---
headers = {"User-Agent": ua, "Pragma": "DFI"}
try:
# 发送 POST 请求
# urllib3 的 body 参数在 httpx 中对应 content (用于二进制数据)
r = httpx.post(
"http://at.sys-allnet.cn/net/initialize", content=encrypted, headers=headers
)
# 检查响应状态码 (可选,但在 httpx 中推荐)
# r.raise_for_status()
# urllib3 的 r.data 在 httpx 中对应 r.content
resp_data = r.content
# 解密逻辑保持不变
# 注意这里逻辑是用响应的前16字节作为IV同时解密整个数据然后丢弃前16字节
if len(resp_data) >= 16:
decrypted = dec(key, resp_data[:16], resp_data)
decrypted_bytes = decrypted[16:]
decrypted_str = unquote(decrypted_bytes.decode("UTF-8"), "utf-8")
print(f"Decrypted: {decrypted_str}")
else:
print("Response data too short.")
except httpx.RequestError as e:
print(f"An error occurred while requesting: {e}")
if __name__ == "__main__":
hello()

83
src/sdgb/UpsertMusic.py Normal file
View File

@@ -0,0 +1,83 @@
import json
import asyncio
import logging
import httpx
from sdgb.sdgb import MaimaiClient
from sdgb.settings import userId, musicData
from sdgb.payload import (
requestData_UserLogin,
requestData_UserLogout,
requestData_UserPreview,
requestData_UserData,
UserAll_payload,
)
maimai = MaimaiClient()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
async def run_workflow(self: MaimaiClient):
async with httpx.AsyncClient(verify=False) as client:
# Preview 探测
PreviewResponse = json.loads(
await self.call_api(
client, "GetUserPreviewApi", requestData_UserPreview, userId
)
)
if PreviewResponse["isLogin"]:
logger.error("已在他处登录。")
return
# UserLogin
LoginResponse = json.loads(
await self.call_api(client, "UserLoginApi", requestData_UserLogin, userId)
)
if LoginResponse["returnCode"] != 1:
logger.error("login failed.")
return
loginId = LoginResponse["loginId"]
loginDate = LoginResponse["lastLoginDate"]
# UserData 等
tasks = [
self.call_api(client, "GetUserDataApi", requestData_UserData, userId),
self.call_api(client, "GetUserExtendApi", requestData_UserData, userId),
self.call_api(client, "GetUserOptionApi", requestData_UserData, userId),
self.call_api(client, "GetUserRatingApi", requestData_UserData, userId),
self.call_api(client, "GetUserChargeApi", requestData_UserData, userId),
self.call_api(client, "GetUserActivityApi", requestData_UserData, userId),
self.call_api(
client, "GetUserMissionDataApi", requestData_UserData, userId
),
]
GeneralUserInfo = await asyncio.gather(*tasks)
await asyncio.sleep(60) # 模拟游戏时间
# UserAll
requestData_UserAll = UserAll_payload(
loginId, loginDate, musicData, GeneralUserInfo
)
await self.call_api(client, "UpsertUserAllApi", requestData_UserAll, userId)
# UserLogout
await self.call_api(client, "UserLogoutApi", requestData_UserLogout, userId)
if __name__ == "__main__":
asyncio.run(run_workflow(maimai))

32
src/sdgb/UserLogoutApi.py Normal file
View File

@@ -0,0 +1,32 @@
import asyncio
import httpx
from sdgb.sdgb import MaimaiClient
from sdgb.settings import (
userId,
regionId,
placeId,
clientId,
)
maimai = MaimaiClient()
async def run_workflow(maimai: MaimaiClient):
async with httpx.AsyncClient(verify=False) as client:
data = {
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": 1767000000,
"type": 4,
}
await maimai.call_api(client, "UserLogoutApi", data, userId)
# 执行入口
if __name__ == "__main__":
asyncio.run(run_workflow(maimai))

42
src/sdgb/chime.py Normal file
View File

@@ -0,0 +1,42 @@
import hashlib
import json
from datetime import datetime
import httpx
import pytz
from sdgb.settings import KeychipID
def qr_api(qr_code: str):
if len(qr_code) > 64:
qr_code = qr_code[-64:]
time_stamp = datetime.now(pytz.timezone("Asia/Tokyo")).strftime("%y%m%d%H%M%S")
auth_key = (
hashlib.sha256(
(KeychipID + time_stamp + "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW").encode(
"UTF-8"
)
)
.hexdigest()
.upper()
)
param = {
"chipID": KeychipID,
"openGameID": "MAID",
"key": auth_key,
"qrCode": qr_code,
"timestamp": time_stamp,
}
headers = {
"Contention": "Keep-Alive",
"Host": "ai.sys-all.cn",
"User-Agent": "WC_AIME_LIB",
}
res = httpx.post(
"http://ai.sys-allnet.cn/wc_aime/api/get_data",
data=json.dumps(param, separators=(",", ":")),
headers=headers,
)
assert res.status_code == 200, "网络错误"
return json.loads(res.content)

71
src/sdgb/encrypt.py Normal file
View File

@@ -0,0 +1,71 @@
import hashlib
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
# 1.55 -> 1.56 -- 舞萌 DX 2026
AesKey = "FKM2JX:VjZNK6hc:A0<JU:i5oR7LA]9W"
AesIV = "F>;24DjU9W6ZsRH["
ObfuscateParam = "8bF76dE9"
# 1.50 -> 1.53 -- 舞萌 DX 2025
# AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"
# AesIV = "d6xHIKq]1J]Dt^ue"
# ObfuscateParam = "B44df8yT"
# 1.40 -- 舞萌 DX 2024
# AesKey = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"
# AesIV = ";;KjR1C3hgB1ovXa"
# ObfuscateParam = "BEs2D5vW"
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: 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, content):
cipher = AES.new(self.key, self.mode, self.iv)
decrypted_padded = cipher.decrypt(content)
decrypted = unpad(decrypted_padded, AES.block_size)
return decrypted
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 get_hash_api(api):
return hashlib.md5((api + "MaimaiChn" + ObfuscateParam).encode()).hexdigest()
def CalcRandom():
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

3741
src/sdgb/keychip.csv Normal file

File diff suppressed because it is too large Load Diff

66
src/sdgb/keychip.py Normal file
View File

@@ -0,0 +1,66 @@
from urllib.parse import unquote
import httpx
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def enc(key, iv, data):
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(data)
return encrypted
def dec(key, iv, data):
de_cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = de_cipher.decrypt(data)
return decrypted
def hello():
key = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
# key = bytes([ 45, 97, 53, 55, 85, 88, 52, 121, 57, 47, 104, 40, 73, 109, 65, 81 ])
iv = bytes.fromhex("00000000000000000000000000000000")
ua = "SDGB;Windows/Lite"
# ua = 'SDHJ;Windows/Lite'
# 构建 payload
content = bytes([0] * 16) + b"title_id=SDGB&title_ver=1.52&client_id=A63E01E6149"
print(f"Content: {content}")
header = bytes.fromhex("00000000000000000000000000000000")
bytes_data = pad(header + content, 16)
encrypted = enc(key, iv, bytes_data)
# --- HTTPX 修改部分 ---
headers = {"User-Agent": ua, "Pragma": "DFI"}
try:
# 发送 POST 请求
# urllib3 的 body 参数在 httpx 中对应 content (用于二进制数据)
r = httpx.post(
"http://at.sys-allnet.cn/net/initialize", content=encrypted, headers=headers
)
# 检查响应状态码 (可选,但在 httpx 中推荐)
# r.raise_for_status()
# urllib3 的 r.data 在 httpx 中对应 r.content
resp_data = r.content
# 解密逻辑保持不变
# 注意这里逻辑是用响应的前16字节作为IV同时解密整个数据然后丢弃前16字节
if len(resp_data) >= 16:
decrypted = dec(key, resp_data[:16], resp_data)
decrypted_bytes = decrypted[16:]
decrypted_str = unquote(decrypted_bytes.decode("UTF-8"), "utf-8")
print(f"Decrypted: {decrypted_str}")
else:
print("Response data too short.")
except httpx.RequestError as e:
print(f"An error occurred while requesting: {e}")
if __name__ == "__main__":
hello()

523
src/sdgb/payload.py Normal file
View File

@@ -0,0 +1,523 @@
import time
import json
import logging
from datetime import datetime
import pytz
from encrypt import CalcRandom
from sdgb.chime import qr_api
from sdgb.settings import (
clientId,
qrCode,
regionId,
regionName,
placeId,
placeName,
)
userId = qr_api(qrCode)["userID"]
token = qr_api(qrCode)["token"]
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
TimeStamp = int(time.time())
requestData_UserPreview = {
"userId": userId,
"segaIdAuthKey": "",
"token": token,
"clientId": clientId,
}
requestData_UserLogin = {
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": TimeStamp - 600,
"loginDateTime": TimeStamp,
"isContinue": False,
"genericFlag": 0,
"token": token,
}
requestData_UserData = {"userId": userId}
requestData_UserLogout = {
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"loginDateTime": TimeStamp,
"type": 1,
}
def UserAll_payload(
loginId: int, loginDate: str, musicData: dict, GeneralUserInfo: list
):
userData = json.loads(GeneralUserInfo[0])
userExtend = json.loads(GeneralUserInfo[1])
userOption = json.loads(GeneralUserInfo[2])
userRating = json.loads(GeneralUserInfo[3])
userChargeList = json.loads(GeneralUserInfo[4])
userActivity = json.loads(GeneralUserInfo[5])
userMissionDataList = json.loads(GeneralUserInfo[6])
requestData_UserAll = {
"userId": userId,
"playlogId": loginId,
"isEventMode": False,
"isFreePlay": False,
"loginDateTime": TimeStamp,
"userPlaylogList": [
{
"userId": 0,
"orderId": 0,
"playlogId": loginId,
"version": 1053000,
"placeId": placeId,
"placeName": placeName,
"loginDate": 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": musicData["musicId"],
"level": musicData["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": userData["userData"]["charaSlot"][0],
"characterLevel1": 1,
"characterAwakening1": 0,
"characterId2": userData["userData"]["charaSlot"][1],
"characterLevel2": 1,
"characterAwakening2": 0,
"characterId3": userData["userData"]["charaSlot"][2],
"characterLevel3": 1,
"characterAwakening3": 0,
"characterId4": userData["userData"]["charaSlot"][3],
"characterLevel4": 1,
"characterAwakening4": 0,
"characterId5": userData["userData"]["charaSlot"][4],
"characterLevel5": 1,
"characterAwakening5": 0,
"achievement": musicData["achievement"],
"deluxscore": musicData["deluxscoreMax"],
"scoreRank": musicData["scoreRank"],
"maxCombo": 0,
"totalCombo": 128,
"maxSync": 0,
"totalSync": 0,
"tapCriticalPerfect": 101,
"tapPerfect": 0,
"tapGreat": 0,
"tapGood": 0,
"tapMiss": 0,
"holdCriticalPerfect": 9,
"holdPerfect": 0,
"holdGreat": 0,
"holdGood": 0,
"holdMiss": 0,
"slideCriticalPerfect": 4,
"slidePerfect": 0,
"slideGreat": 0,
"slideGood": 0,
"slideMiss": 0,
"touchCriticalPerfect": 0,
"touchPerfect": 0,
"touchGreat": 0,
"touchGood": 0,
"touchMiss": 0,
"breakCriticalPerfect": 1,
"breakPerfect": 0,
"breakGreat": 0,
"breakGood": 0,
"breakMiss": 0,
"isTap": True,
"isHold": True,
"isSlide": True,
"isTouch": False,
"isBreak": True,
"isCriticalDisp": True,
"isFastLateDisp": True,
"fastCount": 0,
"lateCount": 0,
"isAchieveNewRecord": False,
"isDeluxscoreNewRecord": False,
"comboStatus": musicData["comboStatus"],
"syncStatus": musicData["syncStatus"],
"isClear": True,
"beforeRating": userData["userData"]["playerRating"],
"afterRating": userData["userData"]["playerRating"],
"beforeGrade": 0,
"afterGrade": 0,
"afterGradeRank": 0,
"beforeDeluxRating": userData["userData"]["playerRating"],
"afterDeluxRating": userData["userData"]["playerRating"],
"isPlayTutorial": False,
"isEventMode": False,
"isFreedomMode": False,
"playMode": 0,
"isNewFree": False,
"trialPlayAchievement": -1,
"extNum1": 0,
"extNum2": 0,
"extNum4": 101,
"extBool1": False,
"extBool2": False,
}
],
"upsertUserAll": {
"userData": [
{
"accessCode": "",
"userName": userData["userData"]["userName"],
"isNetMember": 1,
"point": userData["userData"]["point"],
"totalPoint": userData["userData"]["totalPoint"],
"iconId": userData["userData"]["iconId"],
"plateId": userData["userData"]["plateId"],
"titleId": userData["userData"]["titleId"],
"partnerId": userData["userData"]["partnerId"],
"frameId": userData["userData"]["frameId"],
"selectMapId": userData["userData"]["selectMapId"],
"totalAwake": userData["userData"]["totalAwake"],
"gradeRating": userData["userData"]["gradeRating"],
"musicRating": userData["userData"]["musicRating"],
"playerRating": userData["userData"]["playerRating"],
"highestRating": userData["userData"]["highestRating"],
"gradeRank": userData["userData"]["gradeRank"],
"classRank": userData["userData"]["classRank"],
"courseRank": userData["userData"]["courseRank"],
"charaSlot": userData["userData"]["charaSlot"],
"charaLockSlot": userData["userData"]["charaLockSlot"],
"contentBit": userData["userData"]["contentBit"],
"playCount": userData["userData"]["playCount"] + 1,
"currentPlayCount": userData["userData"]["currentPlayCount"] + 1,
"renameCredit": userData["userData"]["renameCredit"],
"mapStock": userData["userData"]["mapStock"],
"eventWatchedDate": userData["userData"]["eventWatchedDate"],
"lastGameId": "SDGB",
"lastRomVersion": userData["userData"]["lastRomVersion"],
"lastDataVersion": userData["userData"]["lastDataVersion"],
"lastLoginDate": loginDate,
"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": userData["userData"]["lastSelectEMoney"],
"lastSelectTicket": userData["userData"]["lastSelectTicket"],
"lastSelectCourse": userData["userData"]["lastSelectCourse"],
"lastCountCourse": userData["userData"]["lastCountCourse"],
"firstGameId": userData["userData"]["firstGameId"],
"firstRomVersion": userData["userData"]["firstRomVersion"],
"firstDataVersion": userData["userData"]["firstDataVersion"],
"firstPlayDate": userData["userData"]["firstPlayDate"],
"compatibleCmVersion": userData["userData"]["compatibleCmVersion"],
"dailyBonusDate": userData["userData"]["dailyBonusDate"],
"dailyCourseBonusDate": userData["userData"][
"dailyCourseBonusDate"
],
"lastPairLoginDate": userData["userData"]["lastPairLoginDate"],
"lastTrialPlayDate": userData["userData"]["lastTrialPlayDate"],
"playVsCount": userData["userData"]["playVsCount"],
"playSyncCount": userData["userData"]["playSyncCount"],
"winCount": userData["userData"]["winCount"],
"helpCount": userData["userData"]["helpCount"],
"comboCount": userData["userData"]["comboCount"],
"totalDeluxscore": userData["userData"]["totalDeluxscore"],
"totalBasicDeluxscore": userData["userData"][
"totalBasicDeluxscore"
],
"totalAdvancedDeluxscore": userData["userData"][
"totalAdvancedDeluxscore"
],
"totalExpertDeluxscore": userData["userData"][
"totalExpertDeluxscore"
],
"totalMasterDeluxscore": userData["userData"][
"totalMasterDeluxscore"
],
"totalReMasterDeluxscore": userData["userData"][
"totalReMasterDeluxscore"
],
"totalSync": userData["userData"]["totalSync"],
"totalBasicSync": userData["userData"]["totalBasicSync"],
"totalAdvancedSync": userData["userData"]["totalAdvancedSync"],
"totalExpertSync": userData["userData"]["totalExpertSync"],
"totalMasterSync": userData["userData"]["totalMasterSync"],
"totalReMasterSync": userData["userData"]["totalReMasterSync"],
"totalAchievement": userData["userData"]["totalAchievement"],
"totalBasicAchievement": userData["userData"][
"totalBasicAchievement"
],
"totalAdvancedAchievement": userData["userData"][
"totalAdvancedAchievement"
],
"totalExpertAchievement": userData["userData"][
"totalExpertAchievement"
],
"totalMasterAchievement": userData["userData"][
"totalMasterAchievement"
],
"totalReMasterAchievement": userData["userData"][
"totalReMasterAchievement"
],
"playerOldRating": userData["userData"]["playerOldRating"],
"playerNewRating": userData["userData"]["playerNewRating"],
"banState": userData["banState"],
"friendRegistSkip": userData["userData"]["friendRegistSkip"],
"dateTime": TimeStamp,
}
],
"userExtend": [userExtend["userExtend"]],
"userOption": [userOption["userOption"]],
"userCharacterList": [],
"userGhost": [],
"userMapList": [],
"userLoginBonusList": [],
"userRatingList": [userRating["userRating"]],
"userItemList": [],
"userMusicDetailList": [musicData],
"userCourseList": [],
"userFriendSeasonRankingList": [],
"userChargeList": userChargeList["userChargeList"],
"userFavoriteList": [
{"itemKind": 3, "itemIdList": []},
{"itemKind": 1, "itemIdList": []},
{"itemKind": 2, "itemIdList": []},
{"itemKind": 10, "itemIdList": []},
{"itemKind": 11, "itemIdList": []},
],
"userActivityList": [userActivity["userActivity"]],
"userMissionDataList": [
{
"type": userMissionDataList["userMissionDataList"][0]["type"],
"difficulty": userMissionDataList["userMissionDataList"][0][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][0][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][0][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][0][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
0
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][0][
"clearFlag"
],
},
{
"type": userMissionDataList["userMissionDataList"][1]["type"],
"difficulty": userMissionDataList["userMissionDataList"][1][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][1][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][1][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][1][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
1
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][1][
"clearFlag"
],
},
{
"type": userMissionDataList["userMissionDataList"][2]["type"],
"difficulty": userMissionDataList["userMissionDataList"][2][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][2][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][2][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][2][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
2
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][2][
"clearFlag"
],
},
{
"type": userMissionDataList["userMissionDataList"][3]["type"],
"difficulty": userMissionDataList["userMissionDataList"][3][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][3][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][3][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][3][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
3
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][3][
"clearFlag"
],
},
{
"type": userMissionDataList["userMissionDataList"][4]["type"],
"difficulty": userMissionDataList["userMissionDataList"][4][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][4][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][4][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][4][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
4
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][4][
"clearFlag"
],
},
{
"type": userMissionDataList["userMissionDataList"][5]["type"],
"difficulty": userMissionDataList["userMissionDataList"][5][
"difficulty"
],
"targetGenreId": userMissionDataList["userMissionDataList"][5][
"targetGenreId"
],
"targetGenreTableId": userMissionDataList["userMissionDataList"][5][
"targetGenreTableId"
],
"conditionGenreId": userMissionDataList["userMissionDataList"][5][
"conditionGenreId"
],
"conditionGenreTableId": userMissionDataList["userMissionDataList"][
5
]["conditionGenreTableId"],
"clearFlag": userMissionDataList["userMissionDataList"][5][
"clearFlag"
],
},
],
"userWeeklyData": {
"lastLoginWeek": userMissionDataList["userWeeklyData"]["lastLoginWeek"],
"beforeLoginWeek": userMissionDataList["userWeeklyData"][
"beforeLoginWeek"
],
"friendBonusFlag": userMissionDataList["userWeeklyData"][
"friendBonusFlag"
],
},
"userGamePlaylogList": [
{
"playlogId": loginId,
"version": userData["userData"]["lastRomVersion"],
"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": 0,
"playSpecial": CalcRandom(),
"playOtherUserId": 0,
}
],
"user2pPlaylog": {
"userId1": 0,
"userId2": 0,
"userName1": "",
"userName2": "",
"regionId": 0,
"placeId": 0,
"user2pPlaylogDetailList": [],
},
"userIntimateList": [],
"userShopItemStockList": [],
"userGetPointList": [],
"userTradeItemList": [],
"userFavoritemusicList": [],
"userKaleidxScopeList": [],
"isNewCharacterList": "",
"isNewMapList": "",
"isNewLoginBonusList": "",
"isNewItemList": "",
"isNewMusicDetailList": "0",
"isNewCourseList": "",
"isNewFavoriteList": "11111",
"isNewFriendSeasonRankingList": "",
"isNewUserIntimateList": "",
"isNewFavoritemusicList": "",
"isNewKaleidxScopeList": "",
},
}
logger.info(
f"🫥 [INFO] userId: '{userId}', loginId: '{loginId}', loginDate: '{loginDate}', timestamp: '{TimeStamp}'"
)
return requestData_UserAll

71
src/sdgb/sdgb.py Normal file
View File

@@ -0,0 +1,71 @@
import json
import logging
import zlib
import httpx
from encrypt import (
AesKey,
AesIV,
aes_pkcs7,
get_hash_api,
)
# 配置日志,方便调试看请求顺序
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class MaimaiClient:
def __init__(self):
self.base_url = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/"
self.aes = aes_pkcs7(AesKey, AesIV)
async def call_api(
self, client: httpx.AsyncClient, ApiType: str, data: dict, userId: int
):
"""
先压缩再加密请求数据,发送请求后解密再解压响应数据
这里的 client 需要传入外部创建的 httpx.AsyncClient 实例
"""
ApiTypeHash = get_hash_api(ApiType)
url = f"{self.base_url}{ApiTypeHash}"
headers = {
"User-Agent": f"{ApiTypeHash}#{userId}",
"Content-Type": "application/json",
"Mai-Encoding": "1.55",
"Accept-Encoding": "",
"Charset": "UTF-8",
"Content-Encoding": "deflate",
"Host": "maimai-gm.wahlap.com:42081",
}
data = bytes(json.dumps(data), encoding="utf-8")
CompressedData = zlib.compress(data)
AESEncrptedData = self.aes.encrypt(CompressedData)
try:
# 这里的 timeout 设置稍微长一点,防止服务端处理慢
resp = await client.post(
url, headers=headers, data=AESEncrptedData, timeout=10.0
)
resp.raise_for_status() # 如果状态码不是 2xx 则抛出异常
AESEncrptedResponse = resp.content
DecryptedData = self.aes.decrypt(AESEncrptedResponse)
UncompressedData = zlib.decompress(DecryptedData).decode("utf-8")
logger.info(f"✅ [SUCCESS] {ApiType} - {UncompressedData}")
return UncompressedData
except httpx.HTTPStatusError as e:
logger.error(f"❌ [HTTP ERROR] {ApiType}: {e.response.status_code}")
return None
except Exception as e:
logger.error(f"❌ [ERROR] {ApiType}: {str(e)}")
return None

93
src/sdgb/ticket.py Normal file
View File

@@ -0,0 +1,93 @@
import json
import asyncio
import httpx
import time
import logging
from sdgb.sdgb import MaimaiClient
from sdgb.settings import userId, musicData
from sdgb.payload import (
requestData_UserPreview,
requestData_UserLogin,
requestData_UserData,
requestData_UserLogout,
UserAll_payload,
)
maimai = MaimaiClient()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
async def run_workflow(self: MaimaiClient):
async with httpx.AsyncClient(verify=False) as client:
# Preview 探测
PreviewResponse = json.loads(
await self.call_api(
client, "GetUserPreviewApi", requestData_UserPreview, userId
)
)
if PreviewResponse["isLogin"]:
logger.error("已在他处登录。")
return
# UserLogin
LoginResponse = json.loads(
await self.call_api(client, "UserLoginApi", requestData_UserLogin, userId)
)
if LoginResponse["returnCode"] == 106:
logger.error("chime verfication failed.")
return
loginId = LoginResponse["loginId"]
loginDate = LoginResponse["lastLoginDate"]
# UserData 等
tasks = [
self.call_api(client, "GetUserDataApi", requestData_UserData, userId),
self.call_api(client, "GetUserExtendApi", requestData_UserData, userId),
self.call_api(client, "GetUserOptionApi", requestData_UserData, userId),
self.call_api(client, "GetUserRatingApi", requestData_UserData, userId),
self.call_api(client, "GetUserChargeApi", requestData_UserData, userId),
self.call_api(client, "GetUserActivityApi", requestData_UserData, userId),
self.call_api(
client, "GetUserMissionDataApi", requestData_UserData, userId
),
]
GeneralUserInfo = await asyncio.gather(*tasks)
time.sleep(60) # 模拟游戏时间
# UserPlaylog
requestData_UserPlaylog = UserPlaylog_payload(
loginId, musicData, GeneralUserInfo[0]
)
await self.call_api(
client, "UploadUserPlaylogListApi", requestData_UserPlaylog, userId
)
# Userall
requestData_Userall = UserAll_payload(
loginId, loginDate, musicData, GeneralUserInfo
)
await self.call_api(client, "UpsertUserAllApi", requestData_Userall, userId)
# UserLogout
await self.call_api(client, "UserLogoutApi", requestData_UserLogout, userId)
if __name__ == "__main__":
asyncio.run(run_workflow(maimai))