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, ApiError>; fn decode(data: impl AsMut<[u8]>) -> Result, 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(api: APIMethod, agent_extra: impl Display, data: D) -> Result 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( client: &AsyncClient, api: APIMethod, agent_extra: impl Display + Send + 'static, data: D, ) -> impl Future, 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( client: &AsyncClient, api: APIMethod, agent_extra: impl Display + Send + 'static, data: D, ) -> impl Future> 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( client: &AsyncClient, data: M::Payload, agent_extra: impl Display + Send + 'static, ) -> impl Future> { Self::request(client, M::METHOD, agent_extra, data) } } pub struct Sdgb1_53; impl MaiVersion for Sdgb1_53 { const AES_KEY: &[u8; 32] = b"o2U8F6"; const AES_IV: &[u8; 16] = b"AL