Files
sdgb-utils-rs/sdgb-api/src/title/mod.rs

147 lines
4.2 KiB
Rust

use std::fmt::Display;
use crate::title::methods::{APIExt, APIMethod};
pub mod encryption;
pub mod methods;
pub mod model;
pub mod helper;
use super::ApiError;
use nyquest::{
AsyncClient, Body,
r#async::Request,
header::{ACCEPT, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_TYPE, COOKIE, EXPECT, USER_AGENT},
};
use serde::{Deserialize, Serialize};
use spdlog::debug;
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 mut 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, "*/*")
.with_header(ACCEPT_ENCODING, "")
.with_header("Charset", "UTF-8")
.with_header(CONTENT_ENCODING, "deflate")
.with_header(CONTENT_TYPE, "application/json")
.with_header(EXPECT, "100-continue");
// TODO: userid, token
if Self::VERSION >= "1.53" && false {
req = req.with_header(COOKIE, format!(""))
}
debug!("request: {req:?}");
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;
#[cfg(all(not(feature = "compio"), not(feature = "tokio")))]
compile_error!("you must enable one of `compio` or `tokio`");
async {
let req = spawn_blocking(move || Self::api_call(api, agent_extra, data))
.await
.map_err(|_| ApiError::JoinError)??;
let resp = client.request(req).await?.with_successful_status()?;
debug!(
"server response: {}, {:?} bytes",
resp.status(),
resp.content_length()
);
let data = resp.bytes().await?;
debug!("server response payload: {data:?}");
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)?)
}
}
fn request_ext<M: APIExt>(
client: &AsyncClient,
data: M::Payload,
agent_extra: impl Display + Send + 'static,
) -> impl Future<Output = Result<M::Response, ApiError>> {
Self::request(client, M::METHOD, agent_extra, data)
}
}
pub struct Sdgb1_53;
impl MaiVersion for Sdgb1_53 {
const AES_KEY: &[u8; 32] = b"o2U8F6<adcYl25f_qwx_n]5_qxRcbLN>";
const AES_IV: &[u8; 16] = b"AL<G:k:X6Vu7@_U]";
const OBFUSECATE_SUFFIX: &str = "MaimaiChnLatuAa81";
const VERSION: &str = "1.53";
}