feat: initial support of method call

This commit is contained in:
mokurin000
2025-07-30 02:33:31 +08:00
parent d870dc7047
commit 6ee009715d
8 changed files with 179 additions and 8 deletions

35
Cargo.lock generated
View File

@@ -718,6 +718,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "md5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.5" version = "2.7.5"
@@ -1098,10 +1104,12 @@ dependencies = [
"digest", "digest",
"flate2", "flate2",
"hmac-sha256", "hmac-sha256",
"md5",
"nyquest", "nyquest",
"serde", "serde",
"serde_json", "serde_json",
"snafu", "snafu",
"strum 0.27.2",
] ]
[[package]] [[package]]
@@ -1205,8 +1213,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15bf745ed831fe29ec1ff6cc8dfb443721c08e895c9a08fcaa1e2c6f09ec020d" checksum = "15bf745ed831fe29ec1ff6cc8dfb443721c08e895c9a08fcaa1e2c6f09ec020d"
dependencies = [ dependencies = [
"nom", "nom",
"strum", "strum 0.24.1",
"strum_macros", "strum_macros 0.24.3",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@@ -1260,7 +1268,16 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.24.3",
]
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.2",
] ]
[[package]] [[package]]
@@ -1276,6 +1293,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"

View File

@@ -11,12 +11,18 @@ snafu = { workspace = true }
digest = "0.10.7" digest = "0.10.7"
hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] } hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] }
# other utils
chrono = "0.4.41" chrono = "0.4.41"
strum = { version = "0.27.2", features = ["derive"] }
# network request
nyquest = { version = "0.2.0", features = ["async", "json"] } nyquest = { version = "0.2.0", features = ["async", "json"] }
# (de)serialization
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0.141"
flate2 = "1.1.2" flate2 = "1.1.2"
cbc = "0.1.2" cbc = "0.1.2"
aes = "0.8.4" aes = "0.8.4"
md5 = "0.8.0"

View File

@@ -7,9 +7,14 @@ pub enum ApiError {
PadError { error: PadError }, PadError { error: PadError },
#[snafu(display("unpad data: {error}"))] #[snafu(display("unpad data: {error}"))]
UnpadError { error: UnpadError }, UnpadError { error: UnpadError },
#[snafu(display("io error: {source}"))] #[snafu(display("io error: {source}"))]
#[snafu(context(false))] #[snafu(context(false))]
IOError { source: std::io::Error }, IOError { source: std::io::Error },
#[snafu(display("json error: {source}"))]
#[snafu(context(false))]
JSONError { source: serde_json::Error },
} }
impl From<UnpadError> for ApiError { impl From<UnpadError> for ApiError {

View File

@@ -0,0 +1,63 @@
#[derive(strum::IntoStaticStr)]
pub enum APIMethod {
GetGameChargeApi,
GetGameEventApi,
GetGameNgMusicIdApi,
GetGameNgWordListApi,
GetGameRankingApi,
GetGameSettingApi,
GetGameTournamentInfoApi,
GetTransferFriendApi,
GetUserActivityApi,
GetUserCardApi,
GetUserCharacterApi,
GetUserChargeApi,
GetUserCourseApi,
GetUserDataApi,
GetUserExtendApi,
GetUserFavoriteApi,
GetUserFavoriteItemApi,
GetUserFriendSeasonRankingApi,
GetUserGhostApi,
GetUserItemApi,
GetUserLoginBonusApi,
GetUserMapApi,
GetUserMusicApi,
GetUserOptionApi,
GetUserPortraitApi,
GetUserPreviewApi,
GetUserRatingApi,
GetUserRecommendRateMusicApi,
GetUserRecommendSelectMusicApi,
GetUserRegionApi,
GetUserScoreRankingApi,
Ping,
UploadUserPhotoApi,
UploadUserPlaylogApi,
UploadUserPortraitApi,
UpsertClientBookkeepingApi,
UpsertClientSettingApi,
UpsertClientTestmodeApi,
UpsertClientUploadApi,
UpsertUserAllApi,
UpsertUserChargelogApi,
UserLoginApi,
UserLogoutApi,
}
#[cfg(test)]
mod _test {
use crate::title::{MaiVersionExt, Sdgb1_50, methods::APIMethod};
#[test]
fn test_obfuscate_1_50() {
assert_eq!(
Sdgb1_50::api_hash(APIMethod::Ping),
"250b3482854e7697de7d8eb6ea1fabb1"
);
assert_eq!(
Sdgb1_50::api_hash(APIMethod::GetUserPreviewApi),
"004cf848f96d393a5f2720101e30b93d"
);
}
}

View File

@@ -1,17 +1,67 @@
use crate::title::error::ApiError; use std::fmt::Display;
use crate::title::methods::APIMethod;
pub mod encryption; pub mod encryption;
pub mod methods;
pub mod model;
mod error; mod error;
pub use error::ApiError;
use nyquest::{
Body,
r#async::Request,
header::{ACCEPT_ENCODING, CONTENT_ENCODING, EXPECT, USER_AGENT},
};
use serde::Serialize;
pub trait MaiVersion { pub trait MaiVersion {
const AES_KEY: &[u8; 32]; const AES_KEY: &[u8; 32];
const AES_IV: &[u8; 16]; const AES_IV: &[u8; 16];
const OBFUSECATE_PARAM: &str; const OBFUSECATE_SUFFIX: &str;
const VERSION: &str;
} }
pub trait MaiVersionExt: MaiVersion { pub trait MaiVersionExt: MaiVersion {
fn encode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError>; fn encode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, ApiError>;
fn decode(data: impl AsMut<[u8]>) -> Result<Vec<u8>, ApiError>; fn decode(data: impl AsMut<[u8]>) -> Result<Vec<u8>, ApiError>;
fn api_hash(api: APIMethod) -> String {
let api_name: &str = api.into();
let mut md5 = md5::Context::new();
md5.consume(api_name);
md5.consume(Self::OBFUSECATE_SUFFIX);
let digest = md5.finalize();
format!("{digest:x}")
}
fn api_request<D>(
api: APIMethod,
agent_extra: impl Display,
data: D,
) -> Result<Request, ApiError>
where
D: Serialize,
{
let json = serde_json::to_vec(&data)?;
let payload = Self::encode(json)?;
let api_hash = Self::api_hash(api);
let req = Request::post(format!(
"https://maimai-gm.wahlap.com:42081/Maimai2Servlet/{api_hash}"
))
.with_body(Body::json_bytes(payload))
.with_header(USER_AGENT, format!("{api_hash}#{agent_extra}"))
.with_header("Mai-Encoding", Self::VERSION)
.with_header(ACCEPT_ENCODING, "")
.with_header("Charset", "UTF-8")
.with_header(CONTENT_ENCODING, "deflate")
.with_header(EXPECT, "100-continue");
Ok(req)
}
} }
pub struct Sdgb1_40; pub struct Sdgb1_40;
@@ -20,10 +70,14 @@ pub struct Sdgb1_50;
impl MaiVersion for Sdgb1_40 { impl MaiVersion for Sdgb1_40 {
const AES_KEY: &[u8; 32] = b"n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"; const AES_KEY: &[u8; 32] = b"n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@";
const AES_IV: &[u8; 16] = b";;KjR1C3hgB1ovXa"; const AES_IV: &[u8; 16] = b";;KjR1C3hgB1ovXa";
const OBFUSECATE_PARAM: &str = "BEs2D5vW"; const OBFUSECATE_SUFFIX: &str = "MaimaiChnBEs2D5vW";
const VERSION: &str = "1.40";
} }
impl MaiVersion for Sdgb1_50 { impl MaiVersion for Sdgb1_50 {
const AES_KEY: &[u8; 32] = b"a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"; const AES_KEY: &[u8; 32] = b"a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R";
const AES_IV: &[u8; 16] = b"d6xHIKq]1J]Dt^ue"; const AES_IV: &[u8; 16] = b"d6xHIKq]1J]Dt^ue";
const OBFUSECATE_PARAM: &str = "B44df8yT"; const OBFUSECATE_SUFFIX: &str = "MaimaiChnB44df8yT";
const VERSION: &str = "1.50";
} }

View File

@@ -0,0 +1,4 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct Ping;

View File

@@ -10,6 +10,7 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Commands { pub enum Commands {
Ping,
/// Login with QRCode from wechat /// Login with QRCode from wechat
QRLogin { QRLogin {
/// content of the qrcode, only the last 64 characters were used /// content of the qrcode, only the last 64 characters were used

View File

@@ -1,6 +1,9 @@
use nyquest_preset::nyquest::ClientBuilder; use nyquest_preset::nyquest::ClientBuilder;
use palc::Parser; use palc::Parser;
use sdgb_api::all_net::QRCode; use sdgb_api::{
all_net::QRCode,
title::{MaiVersionExt, Sdgb1_50, methods::APIMethod, model::Ping},
};
use spdlog::{error, info}; use spdlog::{error, info};
use crate::commands::Cli; use crate::commands::Cli;
@@ -15,6 +18,12 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
let client = ClientBuilder::default().build_async().await?; let client = ClientBuilder::default().build_async().await?;
match cmd.command { match cmd.command {
commands::Commands::Ping => {
let req = Sdgb1_50::api_request(APIMethod::Ping, "", Ping)?;
let data = client.request(req).await?.bytes().await?;
let decoded = Sdgb1_50::decode(data)?;
info!("resp: {:?}", String::from_utf8_lossy(&decoded));
}
commands::Commands::QRLogin { ref qrcode_content } => { commands::Commands::QRLogin { ref qrcode_content } => {
let qrcode = QRCode { qrcode_content }; let qrcode = QRCode { qrcode_content };
match qrcode.login(&client).await { match qrcode.login(&client).await {