use std::{
collections::BTreeMap,
env,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use rand::RngCore;
use reqwest::{
blocking::Client as HttpClient,
header::{ACCEPT, CONTENT_TYPE, USER_AGENT as USER_AGENT_HEADER},
redirect::Policy,
Url,
};
use serde::{de::DeserializeOwned, Serialize};
use crate::{
client::{decode_response, validate_endpoint},
signer, Result, SendMessageData, SendMessageRequest, SendOtpRequest, UniMessage, UniResponse,
VerifyOtpData, VerifyOtpRequest, DEFAULT_ENDPOINT, USER_AGENT,
};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Clone)]
pub struct UniClient {
access_key_id: String,
access_key_secret: Option<String>,
endpoint: Url,
user_agent: String,
http: HttpClient,
}
impl UniClient {
pub fn new(
access_key_id: impl Into<String>,
access_key_secret: impl Into<String>,
) -> Result<Self> {
Self::builder()
.access_key_id(access_key_id)
.access_key_secret(access_key_secret)
.build()
}
pub fn builder() -> UniClientBuilder {
UniClientBuilder::default()
}
pub fn from_env() -> Result<Self> {
Self::builder().build()
}
pub fn messages(&self) -> MessageService<'_> {
MessageService { client: self }
}
pub fn otp(&self) -> OtpService<'_> {
OtpService { client: self }
}
pub fn request<T, P>(&self, action: &str, data: &P) -> Result<UniResponse<T>>
where
T: DeserializeOwned,
P: Serialize + ?Sized,
{
let response = self
.http
.post(self.signed_url(action)?)
.header(USER_AGENT_HEADER, &self.user_agent)
.header(CONTENT_TYPE, "application/json;charset=utf-8")
.header(ACCEPT, "application/json")
.json(data)
.send()?;
let status = response.status();
let headers = response.headers().clone();
let body = response.text()?;
decode_response(status, headers, body)
}
fn signed_url(&self, action: &str) -> Result<Url> {
let mut query = BTreeMap::from([
("accessKeyId".to_owned(), self.access_key_id.clone()),
("action".to_owned(), action.to_owned()),
]);
if let Some(secret) = &self.access_key_secret {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| crate::UniError::Configuration(error.to_string()))?
.as_secs();
let mut bytes = [0_u8; 8];
rand::thread_rng().fill_bytes(&mut bytes);
signer::sign(&mut query, secret, timestamp, &hex::encode(bytes))?;
}
let mut url = self.endpoint.clone();
url.query_pairs_mut().extend_pairs(query.iter());
Ok(url)
}
}
impl std::fmt::Debug for UniClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("blocking::UniClient")
.field("access_key_id", &self.access_key_id)
.field("endpoint", &self.endpoint)
.field("user_agent", &self.user_agent)
.field("signed", &self.access_key_secret.is_some())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct UniClientBuilder {
access_key_id: Option<String>,
access_key_secret: Option<String>,
endpoint: String,
user_agent: String,
timeout: Duration,
http_client: Option<HttpClient>,
}
impl Default for UniClientBuilder {
fn default() -> Self {
Self {
access_key_id: env::var("UNIMTX_ACCESS_KEY_ID").ok(),
access_key_secret: env::var("UNIMTX_ACCESS_KEY_SECRET").ok(),
endpoint: env::var("UNIMTX_ENDPOINT").unwrap_or_else(|_| DEFAULT_ENDPOINT.to_owned()),
user_agent: USER_AGENT.to_owned(),
timeout: DEFAULT_TIMEOUT,
http_client: None,
}
}
}
impl std::fmt::Debug for UniClientBuilder {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("blocking::UniClientBuilder")
.field("access_key_id", &self.access_key_id)
.field("endpoint", &self.endpoint)
.field("user_agent", &self.user_agent)
.field("timeout", &self.timeout)
.field("signed", &self.access_key_secret.is_some())
.finish_non_exhaustive()
}
}
impl UniClientBuilder {
pub fn access_key_id(mut self, value: impl Into<String>) -> Self {
self.access_key_id = Some(value.into());
self
}
pub fn access_key_secret(mut self, value: impl Into<String>) -> Self {
self.access_key_secret = Some(value.into());
self
}
pub fn unsigned(mut self) -> Self {
self.access_key_secret = None;
self
}
pub fn endpoint(mut self, value: impl Into<String>) -> Self {
self.endpoint = value.into();
self
}
pub fn user_agent(mut self, value: impl Into<String>) -> Self {
self.user_agent = value.into();
self
}
pub fn timeout(mut self, value: Duration) -> Self {
self.timeout = value;
self
}
pub fn http_client(mut self, value: HttpClient) -> Self {
self.http_client = Some(value);
self
}
pub fn build(self) -> Result<UniClient> {
let access_key_id = self
.access_key_id
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
crate::UniError::Configuration("access key ID is required".to_owned())
})?;
let endpoint = validate_endpoint(&self.endpoint)?;
let http = match self.http_client {
Some(client) => client,
None => HttpClient::builder()
.timeout(self.timeout)
.redirect(Policy::none())
.build()?,
};
Ok(UniClient {
access_key_id,
access_key_secret: self
.access_key_secret
.filter(|value| !value.trim().is_empty()),
endpoint,
user_agent: self.user_agent,
http,
})
}
}
#[derive(Clone, Copy)]
pub struct MessageService<'a> {
client: &'a UniClient,
}
impl MessageService<'_> {
pub fn send(&self, params: &SendMessageRequest) -> Result<UniResponse<SendMessageData>> {
self.client.request("sms.message.send", params)
}
pub fn send_with<T, P>(&self, params: &P) -> Result<UniResponse<T>>
where
T: DeserializeOwned,
P: Serialize + ?Sized,
{
self.client.request("sms.message.send", params)
}
}
#[derive(Clone, Copy)]
pub struct OtpService<'a> {
client: &'a UniClient,
}
impl OtpService<'_> {
pub fn send(&self, params: &SendOtpRequest) -> Result<UniResponse<UniMessage>> {
self.client.request("otp.send", params)
}
pub fn verify(&self, params: &VerifyOtpRequest) -> Result<UniResponse<VerifyOtpData>> {
self.client.request("otp.verify", params)
}
}