use std::{borrow::Cow, env};
use file::FileUploadRequest;
use md5::{Digest, Md5};
use reqwest::{
header::CONTENT_TYPE,
multipart::{Form, Part},
};
use serde::Serialize;
use serde_json::Value;
use thiserror::Error;
use time::OffsetDateTime;
mod file;
pub use file::PddFile;
use tracing::trace;
mod public_parameters;
pub use public_parameters::PublicParameters;
pub mod requests;
#[cfg(feature = "pmc-native-tls")]
pub mod pmc;
pub struct Config {
pub client_id: String,
pub client_secret: String,
pub url: String,
pub upload_url: String,
pub access_token: Option<String>,
}
impl Config {
pub fn from_env() -> Result<Self, Error> {
let access_token = if let Ok(token) = env::var("PDD_ACCESS_TOKEN") {
Some(token)
} else {
None
};
let url = if let Ok(url) = env::var("PDD_URL") {
url
} else {
"https://gw-api.pinduoduo.com/api/router".to_string()
};
let upload_url = if let Ok(url) = env::var("PDD_UPLOAD_URL") {
url
} else {
"https://gw-upload.pinduoduo.com/api/upload".to_string()
};
Ok(Config {
client_id: env::var("PDD_CLIENT_ID")?,
client_secret: env::var("PDD_CLIENT_SECRET")?,
access_token,
url,
upload_url,
})
}
}
pub struct Client {
pub config: Config,
pub reqwest: reqwest::Client,
}
pub trait Request {
fn get_type() -> String;
fn get_response_name() -> String;
}
impl Client {
pub fn new(config: Config) -> Self {
Client {
config,
reqwest: reqwest::Client::new(),
}
}
pub fn from_env() -> Result<Self, Error> {
Ok(Client {
config: Config::from_env()?,
reqwest: reqwest::Client::new(),
})
}
pub async fn file_upload<T>(self, req: T) -> Result<Value, Error>
where
T: Request + FileUploadRequest + Serialize,
{
let pub_par = init_public_parameters(
self.config.access_token,
self.config.client_id,
T::get_type(),
);
let pub_parameters = serde_json::to_value(pub_par)?;
let req_value = serde_json::to_value(&req)?;
let Some((name, data)) = req.get_file() else {
return Err(Error::FileNotFoundError);
};
let body = add_sign(&self.config.client_secret, pub_parameters, req_value);
let file_part = Part::bytes(data).file_name(name);
let bodya = body.as_object().unwrap();
let mut url = String::new();
for i in bodya {
let value = match i.1 {
Value::String(s) => s.as_str().to_string(),
Value::Null => continue,
_ => i.1.to_string(),
};
url.push_str(&format!("{}={}&", i.0, value));
}
let form = Form::new().part("file", file_part);
trace!("request body:{:?}", form);
let request_url = format!("{}?{}", self.config.upload_url, url);
trace!("{}", request_url);
let rsp = self
.reqwest
.post(&request_url)
.multipart(form)
.send()
.await?;
let result = rsp.text().await?;
trace!("response body:{:?}", result);
Ok(serde_json::from_str(&result)?)
}
pub async fn send<T: Request + Serialize>(self, req: T) -> Result<Value, Error> {
let pub_par = init_public_parameters(
self.config.access_token,
self.config.client_id,
T::get_type(),
);
let pub_parameters = serde_json::to_value(pub_par)?;
let req_value = serde_json::to_value(req)?;
let body = add_sign(&self.config.client_secret, pub_parameters, req_value);
trace!("request body:{:?}", body);
let rsp = self
.reqwest
.post(self.config.url)
.header(CONTENT_TYPE, "application/json;charset=utf-8")
.json(&body)
.send()
.await?;
let result = rsp.json::<Value>().await?;
trace!("response body:{:?}", result);
Ok(result)
}
}
fn init_public_parameters(
access_token: Option<String>,
client_id: String,
type_name: String,
) -> PublicParameters {
PublicParameters {
access_token,
client_id,
data_type: Some("JSON".to_string()),
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
type_: type_name,
version: None,
}
}
fn add_sign(client_secret: &str, mut pub_parameters: Value, mut parameters: Value) -> Value {
let parameters_map = parameters.as_object_mut().unwrap();
parameters_map.append(pub_parameters.as_object_mut().unwrap());
let mut pub_parameters_map: Vec<(&String, &Value)> =
parameters_map.iter().map(|f| (f.0, f.1)).collect();
pub_parameters_map.sort_by(|a, b| a.0.bytes().cmp(b.0.bytes()));
let mut sign: Vec<Cow<str>> = Vec::new();
sign.push(Cow::Borrowed(client_secret));
for (k, v) in pub_parameters_map {
let value = match v {
Value::String(s) => Cow::Borrowed(s.as_str()),
Value::Null => continue,
_ => Cow::Owned(v.to_string()),
};
sign.push(Cow::Borrowed(k));
sign.push(value.clone());
}
sign.push(Cow::Borrowed(client_secret));
let mut h = Md5::new();
h.update(sign.concat());
let md5 = h.finalize();
parameters_map.insert("sign".to_string(), Value::String(format!("{:X}", md5)));
Value::Object(parameters_map.to_owned())
}
#[derive(Error, Debug)]
pub enum Error {
#[error("没有文件")]
FileNotFoundError,
#[error("使用环境变量初始化Client失败")]
EnvVarNotFoundError(#[from] env::VarError),
#[error("Json解析错误")]
JsonParsingFailure(#[from] serde_json::Error),
#[error("文件读取错误")]
FileIOFailure(#[from] std::io::Error),
#[error("请求失败")]
RequestFailure(#[from] reqwest::Error),
#[cfg(feature = "pmc-native-tls")]
#[error("消息服务连接失败")]
PmcConnectionFailure,
#[cfg(feature = "pmc-native-tls")]
#[error("发生错误")]
PmcFailure,
#[cfg(feature = "pmc-native-tls")]
#[error("消息解析错误")]
PmcMessageFailure,
}
#[cfg(test)]
mod tests {
use crate::requests::PddOrderInformationGet;
use super::*;
#[tokio::test]
async fn exec_test() {
let client = Client::from_env().unwrap();
let req = PddOrderInformationGet {
order_sn: Some("230626-434073824910838".to_string()),
};
client.send(req).await.unwrap();
}
#[test]
fn build_sign_test() {}
}