use crate::{session::SessionStore, client::APIClient, request::{Method, RequestType, LabraResponse, LabraRequest, RequestMethod}, WechatCrypto, util::current_timestamp, LabradorResult, SimpleStorage, WechatRequest, WechatCommonResponse, JsapiSignature, get_timestamp, get_nonce_str};
use serde::{Serialize, Deserialize};
use serde_json::{json, Value};
use crate::wechat::mp::method::WechatMpMethod;
mod api;
mod method;
pub mod events;
pub mod messages;
pub mod replies;
#[allow(unused)]
mod constants;
mod msg_parser;
pub use api::*;
pub use msg_parser::parse_message;
use crate::wechat::mp::constants::{ACCESS_TOKEN, APPID, CLIENT_CREDENTIAL, GRANT_TYPE, SECRET, TICKET_TYPE, TICKET_TYPE_JSAPI, TICKET_TYPE_SDK, TICKET_TYPE_WXCARD};
use crate::wechat::mp::method::WechatMpMethod::QrConnectUrl;
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct WechatMpClient<T: SessionStore> {
appid: String,
secret: String,
token: Option<String>,
template_id: Option<String>,
aes_key: Option<String>,
client: APIClient<T>,
}
#[allow(unused)]
#[derive(Serialize, Deserialize)]
pub struct AccessTokenResponse{
pub access_token: String,
pub expires_in: i64,
}
#[allow(unused)]
#[derive(Serialize, Deserialize)]
pub struct WechatMpShortKeyResponse{
pub long_data: Option<String>,
pub create_time: Option<i64>,
pub expire_seconds: Option<i64>,
}
pub enum TicketType {
JSAPI,
SDK,
WxCard
}
impl ToString for TicketType {
fn to_string(&self) -> String {
match self {
TicketType::JSAPI => TICKET_TYPE_JSAPI.to_string(),
TicketType::SDK => TICKET_TYPE_SDK.to_string(),
TicketType::WxCard => TICKET_TYPE_WXCARD.to_string(),
}
}
}
#[allow(unused)]
impl<T: SessionStore> WechatMpClient<T> {
fn from_client(client: APIClient<T>) -> WechatMpClient<T> {
WechatMpClient {
appid: client.app_key.to_owned(),
secret: client.secret.to_owned(),
token: None,
template_id: None,
aes_key: None,
client
}
}
pub fn new<S: Into<String>>(appid: S, secret: S) -> WechatMpClient<SimpleStorage> {
let client = APIClient::<SimpleStorage>::from_session(appid.into(), secret.into(), "https://api.weixin.qq.com", SimpleStorage::new());
WechatMpClient::from_client(client)
}
pub fn from_session<S: Into<String>>(appid: S, secret: S, session: T) -> WechatMpClient<T> {
let client = APIClient::from_session(appid.into(), secret.into(), "https://api.weixin.qq.com", session);
Self::from_client(client)
}
pub fn aes_key(mut self, aes_key: &str) -> Self {
self.aes_key = aes_key.to_string().into();
self
}
pub fn token(mut self, token: &str) -> Self {
self.token = token.to_string().into();
self
}
pub fn template_id(mut self, template_id: &str) -> Self {
self.template_id = template_id.to_string().into();
self
}
#[inline]
pub async fn access_token(&self, force_refresh: bool) -> LabradorResult<String> {
let session = self.client.session();
let token_key = format!("{}_access_token", self.appid);
let expires_key = format!("{}_expires_at", self.appid);
let token: String = session.get(&token_key, Some("".to_owned()))?.unwrap_or_default();
let timestamp = current_timestamp();
let expires_at: i64 = session.get(&expires_key, Some(timestamp))?.unwrap_or_default();
if expires_at <= timestamp || force_refresh {
let mut req = LabraRequest::<String>::new().url(WechatMpMethod::AccessToken.get_method()).params(vec![
(GRANT_TYPE.to_string(), CLIENT_CREDENTIAL.to_string()),
(APPID.to_string(), self.client.app_key.to_string()),
(SECRET.to_string(), self.client.secret.to_string()),
]).method(Method::Get).req_type(RequestType::Json);
let v = self.client.request(req).await?.json::<Value>()?;
let res = WechatCommonResponse::parse::<AccessTokenResponse>(v)?;
let token = res.access_token;
let expires_in = res.expires_in;
let expires_at = current_timestamp() + expires_in - 200;
session.set(&token_key, token.to_owned(), Some(expires_in as usize));
session.set(&expires_key, expires_at, Some(expires_in as usize));
Ok(token)
} else {
Ok(token)
}
}
#[inline]
pub async fn gen_shorten(&self, long_data: &str, expire_seconds: u64) -> LabradorResult<String> {
let res = self.post(WechatMpMethod::GenShortenUrl, vec![], json!({"long_data": long_data, "expire_seconds": expire_seconds}), RequestType::Json).await?.json::<Value>()?;
let v = WechatCommonResponse::parse::<Value>(res)?;
let short_key = v["short_key"].as_str().unwrap_or_default();
Ok(short_key.to_string())
}
#[inline]
pub async fn fetch_shorten(&self, short_key: &str) -> LabradorResult<WechatMpShortKeyResponse> {
let res = self.post(WechatMpMethod::GenShortenUrl, vec![], json!({"short_key": short_key}), RequestType::Json).await?.json::<Value>()?;
WechatCommonResponse::parse::<WechatMpShortKeyResponse>(res)
}
#[inline]
pub async fn get_ticket(&self, ticket_type: TicketType) -> LabradorResult<String> {
self.get_ticket_force(ticket_type, false).await
}
#[inline]
pub async fn get_ticket_force(&self, ticket_type: TicketType, force_refresh: bool) -> LabradorResult<String> {
let session = self.client.session();
let key = format!("{}_{}_ticket", self.appid, &ticket_type.to_string());
let expires_key = format!("{}_{}_ticket_expires_at", self.appid, &ticket_type.to_string());
let ticket: String = session.get(&key, Some("".to_owned()))?.unwrap_or_default();
let timestamp = current_timestamp();
let expires_at: i64 = session.get(&expires_key, Some(timestamp))?.unwrap_or_default();
if expires_at <= timestamp || force_refresh {
let res = self.get(WechatMpMethod::GetTicket, vec![(TICKET_TYPE.to_string(), ticket_type.to_string())], RequestType::Json).await?.json::<Value>()?;
let v = WechatCommonResponse::parse::<Value>(res)?;
let ticket = v["ticket"].as_str().unwrap_or_default();
let expires_in = v["expires_in"].as_i64().unwrap_or_default();
let expires_at = current_timestamp() + expires_in - 200;
session.set(&key, ticket.to_string(), Some(expires_in as usize));
session.set(&expires_key, expires_at, Some(expires_in as usize));
Ok(ticket.to_string())
} else {
Ok(ticket)
}
}
pub async fn create_jsapi_signature(&self, url: &str) -> LabradorResult<JsapiSignature> {
let timestamp = get_timestamp() / 1000;
let noncestr = get_nonce_str();
let jsapi_ticket = self.get_jsapi_ticket(false).await?;
let signature = WechatCrypto::get_sha1_sign(&vec!["jsapi_ticket=".to_string() + &jsapi_ticket,
"noncestr=".to_string() + &noncestr,
"timestamp=".to_string() + ×tamp.to_string(),"url=".to_string() + &url].join("&"));
Ok(JsapiSignature{
app_id: self.appid.to_string(),
nonce_str: noncestr,
url: url.to_string(),
signature,
timestamp,
})
}
pub async fn build_qr_connect_url(&self, redirect_url: &str, scope: &str, state: &str, ) -> LabradorResult<String> {
Ok(format!("{}?appid={}&redirect_uri={}&response_type=code&scope={}&state={}#wechat_redirect", QrConnectUrl.get_method(), self.appid.to_string(), urlencoding::encode(redirect_url), scope, state))
}
pub async fn get_callback_ip(&self, force_refresh: bool) -> LabradorResult<Vec<String>> {
let v = self.get(WechatMpMethod::GetCallbackIp, vec![], RequestType::Json).await?.json::<Value>()?;
let v = WechatCommonResponse::parse::<Value>(v)?;
let ip_list = v["ip_list"].as_array().unwrap_or(&vec![]).iter().map(|v| v.as_str().unwrap_or_default().to_string()).collect::<Vec<String>>();
Ok(ip_list)
}
pub async fn get_jsapi_ticket(&self, force_refresh: bool) -> LabradorResult<String> {
self.get_ticket_force(TicketType::JSAPI, force_refresh).await
}
pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult<bool> {
let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default());
let _ = crp.check_signature(signature, timestamp, nonce, "")?;
Ok(true)
}
async fn post<D: Serialize>(&self, method: WechatMpMethod, mut querys: Vec<(String, String)>, data: D, request_type: RequestType) -> LabradorResult<LabraResponse> {
let access_token = self.access_token(false).await?;
if !access_token.is_empty() && method.need_token() {
querys.push((ACCESS_TOKEN.to_string(), access_token));
}
self.client.post(method, querys, data, request_type).await
}
async fn execute<D: WechatRequest, B: Serialize>(&self, request: D) -> LabradorResult<LabraResponse> {
let mut querys = request.get_query_params();
if request.is_need_token() {
let access_token = self.access_token(false).await?;
if !access_token.is_empty() {
querys.insert(ACCESS_TOKEN.to_string(), access_token);
}
}
let params = querys.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect::<Vec<(String, String)>>();
let mut req = LabraRequest::<B>::new().url(request.get_api_method_name())
.params(params).method(request.get_request_method()).req_type(request.get_request_type()).body(request.get_request_body::<B>());
self.client.request(req).await
}
async fn get(&self, method: WechatMpMethod, mut params: Vec<(String, String)>, request_type: RequestType) -> LabradorResult<LabraResponse> {
let access_token = self.access_token(false).await?;
if !access_token.is_empty() && method.need_token() {
params.push((ACCESS_TOKEN.to_string(), access_token));
}
self.client.get(method, params, request_type).await
}
pub fn user(&self) -> WechatMpUser<T> {
WechatMpUser::new(self)
}
pub fn oauth2(&self) -> WechatMpOauth2<T> {
WechatMpOauth2::new(self)
}
pub fn qrcode(&self) -> WechatMpQRCode<T> {
WechatMpQRCode::new(self)
}
pub fn custom_service(&self) -> WechatMpCustomService<T> {
WechatMpCustomService::new(self)
}
pub fn menu(&self) -> WechatMpMenu<T> {
WechatMpMenu::new(self)
}
pub fn media(&self) -> WechatMpMedia<T> {
WechatMpMedia::new(self)
}
pub fn template_msg(&self) -> WechatMpTemplateMessage<T> {
WechatMpTemplateMessage::new(self)
}
pub fn subscribe_msg(&self) -> WechatMpSubscribeMessage<T> {
WechatMpSubscribeMessage::new(self)
}
pub fn wifi(&self) -> WechatMpWifi<T> {
WechatMpWifi::new(self)
}
pub fn ocr(&self) -> WechatMpOcr<T> {
WechatMpOcr::new(self)
}
}