Files
sdgb-utils-rs/sdgb-api/src/title/mod.rs
2025-07-30 17:55:19 +08:00

122 lines
3.4 KiB
Rust

use std::fmt::Display;
use crate::title::methods::APIMethod;
pub mod encryption;
pub mod methods;
pub mod model;
use super::ApiError;
use nyquest::{
AsyncClient, Body,
r#async::Request,
header::{ACCEPT_ENCODING, CONTENT_ENCODING, EXPECT, USER_AGENT},
};
use serde::{Deserialize, Serialize};
pub trait MaiVersion {
const AES_KEY: &[u8; 32];
const AES_IV: &[u8; 16];
const OBFUSECATE_SUFFIX: &str;
const VERSION: &str;
}
pub trait MaiVersionExt: MaiVersion {
fn encode(data: impl AsRef<[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_call<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)
}
fn request_raw<D>(
client: &AsyncClient,
api: APIMethod,
agent_extra: impl Display + Send + 'static,
data: D,
) -> impl Future<Output = Result<Vec<u8>, ApiError>>
where
D: Serialize + Send + 'static,
{
#[cfg(feature = "compio")]
use compio::runtime::spawn_blocking;
#[cfg(feature = "tokio")]
use tokio::task::spawn_blocking;
async {
let req = spawn_blocking(move || Self::api_call(api, agent_extra, data))
.await
.map_err(|_| ApiError::JoinError)??;
let data = client.request(req).await?.bytes().await?;
let decoded = spawn_blocking(move || Self::decode(data))
.await
.map_err(|_| ApiError::JoinError)??;
Ok(decoded)
}
}
fn request<D, R>(
client: &AsyncClient,
api: APIMethod,
agent_extra: impl Display + Send + 'static,
data: D,
) -> impl Future<Output = Result<R, ApiError>>
where
D: Serialize + Send + 'static,
R: for<'a> Deserialize<'a>,
{
async {
let raw_data = Self::request_raw(client, api, agent_extra, data).await?;
Ok(serde_json::from_slice(&raw_data)?)
}
}
}
pub struct Sdgb1_40;
pub struct Sdgb1_50;
impl MaiVersion for Sdgb1_40 {
const AES_KEY: &[u8; 32] = b"n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@";
const AES_IV: &[u8; 16] = b";;KjR1C3hgB1ovXa";
const OBFUSECATE_SUFFIX: &str = "MaimaiChnBEs2D5vW";
const VERSION: &str = "1.40";
}
impl MaiVersion for Sdgb1_50 {
const AES_KEY: &[u8; 32] = b"a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R";
const AES_IV: &[u8; 16] = b"d6xHIKq]1J]Dt^ue";
const OBFUSECATE_SUFFIX: &str = "MaimaiChnB44df8yT";
const VERSION: &str = "1.50";
}