dingtalk_stream/transport/
http.rs1use crate::error::{Error, Result};
4use reqwest::header::{HeaderMap, HeaderValue};
5use url::form_urlencoded;
6
7const DEFAULT_OPENAPI_ENDPOINT: &str = "https://api.dingtalk.com";
9const UPLOAD_ENDPOINT: &str = "https://oapi.dingtalk.com";
11
12#[derive(Clone)]
14pub struct HttpClient {
15 client: reqwest::Client,
16 openapi_endpoint: String,
17}
18
19impl HttpClient {
20 pub fn new() -> Self {
22 let openapi_endpoint = std::env::var("DINGTALK_OPENAPI_ENDPOINT")
23 .unwrap_or_else(|_| DEFAULT_OPENAPI_ENDPOINT.to_owned());
24
25 Self {
26 client: reqwest::Client::new(),
27 openapi_endpoint,
28 }
29 }
30
31 pub fn openapi_endpoint(&self) -> &str {
33 &self.openapi_endpoint
34 }
35
36 fn user_agent() -> String {
38 format!(
39 "DingTalkStream/1.0 SDK/{} Rust/{}",
40 env!("CARGO_PKG_VERSION"),
41 rustc_version()
42 )
43 }
44
45 fn build_headers(access_token: Option<&str>) -> HeaderMap {
47 let mut headers = HeaderMap::new();
48 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
49 headers.insert("Accept", HeaderValue::from_static("*/*"));
50 headers.insert(
51 "User-Agent",
52 HeaderValue::from_str(&Self::user_agent())
53 .unwrap_or_else(|_| HeaderValue::from_static("DingTalkStream/1.0")),
54 );
55 if let Some(token) = access_token {
56 if let Ok(val) = HeaderValue::from_str(token) {
57 headers.insert("x-acs-dingtalk-access-token", val);
58 }
59 }
60 headers
61 }
62
63 pub async fn post_json(
65 &self,
66 url: &str,
67 body: &serde_json::Value,
68 access_token: Option<&str>,
69 ) -> Result<serde_json::Value> {
70 let resp = self
71 .client
72 .post(url)
73 .headers(Self::build_headers(access_token))
74 .json(body)
75 .send()
76 .await?;
77
78 let status = resp.status();
79 let text = resp.text().await?;
80
81 if !status.is_success() {
82 return Err(Error::Connection(format!(
83 "POST {} failed with status {}: {}",
84 url, status, text
85 )));
86 }
87
88 serde_json::from_str(&text).map_err(Error::Json)
89 }
90
91 pub async fn put_json(
93 &self,
94 url: &str,
95 body: &serde_json::Value,
96 access_token: Option<&str>,
97 ) -> Result<serde_json::Value> {
98 let resp = self
99 .client
100 .put(url)
101 .headers(Self::build_headers(access_token))
102 .json(body)
103 .send()
104 .await?;
105
106 let status = resp.status();
107 let text = resp.text().await?;
108
109 if !status.is_success() {
110 return Err(Error::Connection(format!(
111 "PUT {} failed with status {}: {}",
112 url, status, text
113 )));
114 }
115
116 serde_json::from_str(&text).or_else(|_| Ok(serde_json::Value::Null))
117 }
118
119 pub async fn post_json_raw(
121 &self,
122 url: &str,
123 body: &serde_json::Value,
124 ) -> Result<(u16, String)> {
125 let resp = self
126 .client
127 .post(url)
128 .header("Content-Type", "application/json")
129 .json(body)
130 .send()
131 .await?;
132
133 let status = resp.status().as_u16();
134 let text = resp.text().await?;
135 Ok((status, text))
136 }
137
138 pub async fn get_bytes(&self, url: &str) -> Result<Vec<u8>> {
140 let resp = self.client.get(url).send().await?;
141 resp.error_for_status_ref()
142 .map_err(|e| Error::Http(e.without_url()))?;
143 let bytes = resp.bytes().await?;
144 Ok(bytes.to_vec())
145 }
146
147 pub async fn upload_file(
149 &self,
150 access_token: &str,
151 content: &[u8],
152 filetype: &str,
153 filename: &str,
154 mimetype: &str,
155 ) -> Result<String> {
156 let encoded_token: String = form_urlencoded::Serializer::new(String::new())
157 .append_pair("access_token", access_token)
158 .finish();
159 let url = format!("{}/media/upload?{}", UPLOAD_ENDPOINT, encoded_token);
160
161 let part = reqwest::multipart::Part::bytes(content.to_vec())
162 .file_name(filename.to_owned())
163 .mime_str(mimetype)
164 .map_err(|e| Error::Handler(format!("invalid mime type: {e}")))?;
165
166 let form = reqwest::multipart::Form::new()
167 .text("type", filetype.to_owned())
168 .part("media", part);
169
170 let resp = self.client.post(&url).multipart(form).send().await?;
171
172 let status = resp.status();
173 let text = resp.text().await?;
174
175 if status.as_u16() == 401 {
176 return Err(Error::Auth("upload returned 401".to_owned()));
177 }
178
179 if !status.is_success() {
180 return Err(Error::Connection(format!(
181 "upload failed with status {}: {}",
182 status, text
183 )));
184 }
185
186 let json: serde_json::Value = serde_json::from_str(&text)?;
187 json.get("media_id")
188 .and_then(|v| v.as_str())
189 .map(String::from)
190 .ok_or_else(|| Error::Handler(format!("upload failed, response: {text}")))
191 }
192
193 pub async fn post_raw(&self, url: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
195 let headers = Self::build_headers(None);
196 let resp = self
197 .client
198 .post(url)
199 .headers(headers)
200 .json(body)
201 .send()
202 .await?;
203
204 let status = resp.status();
205 let text = resp.text().await?;
206
207 if !status.is_success() {
208 return Err(Error::Connection(format!(
209 "POST {} failed with status {}: {}",
210 url, status, text
211 )));
212 }
213
214 serde_json::from_str(&text).map_err(Error::Json)
215 }
216}
217
218impl Default for HttpClient {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224fn rustc_version() -> &'static str {
226 env!("CARGO_PKG_RUST_VERSION")
228}