1use crate::{SlackApiError, SlackError};
2use bytes::Bytes;
3use reqwest::header::{CONTENT_TYPE, RETRY_AFTER};
4use reqwest::{Response, StatusCode};
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::time::Duration;
9
10const DEFAULT_BASE_URL: &str = "https://slack.com/api/";
11const DEFAULT_MAX_RETRIES: u32 = 0;
13const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
15const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(1);
18const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
19
20pub trait SlackApiMethod: Serialize {
22 const METHOD: &'static str;
24 type Response: DeserializeOwned;
26}
27
28pub trait CursorPaginated: SlackApiMethod<Response: NextCursor> {
30 fn set_cursor(&mut self, cursor: String);
31}
32
33pub trait NextCursor {
35 fn next_cursor(&self) -> Option<&str>;
37}
38
39#[derive(Clone)]
59pub struct SlackClient {
60 inner: Arc<Inner>,
61}
62
63struct Inner {
64 http: reqwest::Client,
65 token: Option<String>,
66 base_url: String,
67 max_retries: u32,
68}
69
70impl std::fmt::Debug for SlackClient {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("SlackClient")
73 .field("token", &self.inner.token.as_ref().map(|_| "***"))
74 .field("base_url", &self.inner.base_url)
75 .field("max_retries", &self.inner.max_retries)
76 .finish()
77 }
78}
79
80impl SlackClient {
81 pub fn new(token: impl Into<String>) -> Self {
88 Self::builder().token(token).build()
89 }
90
91 pub fn builder() -> SlackClientBuilder {
92 SlackClientBuilder::default()
93 }
94
95 pub fn with_token(&self, token: impl Into<String>) -> Self {
98 Self {
99 inner: Arc::new(Inner {
100 http: self.inner.http.clone(),
101 token: Some(token.into()),
102 base_url: self.inner.base_url.clone(),
103 max_retries: self.inner.max_retries,
104 }),
105 }
106 }
107
108 pub async fn call<M: SlackApiMethod>(&self, request: &M) -> Result<M::Response, SlackError> {
115 let (status, body) = self.post(M::METHOD, request).await?;
116 decode(status, &body)
117 }
118
119 pub async fn call_raw<P: Serialize + ?Sized>(
136 &self,
137 method: &str,
138 params: &P,
139 ) -> Result<serde_json::Value, SlackError> {
140 let (status, body) = self.post(method, params).await?;
141 decode(status, &body)
142 }
143
144 pub fn pages<M: CursorPaginated>(&self, request: M) -> Pages<'_, M> {
147 Pages {
148 client: self,
149 request: Some(request),
150 }
151 }
152
153 pub(crate) async fn call_bytes<P: Serialize + ?Sized>(
156 &self,
157 method: &str,
158 params: &P,
159 ) -> Result<Bytes, SlackError> {
160 let (status, body) = self.post(method, params).await?;
161 if is_json(&body) {
162 decode::<serde::de::IgnoredAny>(status, &body)?;
163 }
164 if !status.is_success() {
165 return Err(SlackError::Http {
166 status: status.as_u16(),
167 body: String::from_utf8_lossy(&body).into_owned(),
168 });
169 }
170 Ok(body)
171 }
172
173 pub(crate) fn http(&self) -> &reqwest::Client {
174 &self.inner.http
175 }
176
177 async fn post<P: Serialize + ?Sized>(
179 &self,
180 method: &str,
181 params: &P,
182 ) -> Result<(StatusCode, Bytes), SlackError> {
183 let form = crate::form::to_form(params).map_err(|e| SlackError::Encode(e.to_string()))?;
184 let body = Bytes::from(form);
185 let url = format!("{}{}", self.inner.base_url, method);
186 let mut attempt = 0;
187 loop {
188 let mut request = self
189 .inner
190 .http
191 .post(&url)
192 .header(CONTENT_TYPE, FORM_CONTENT_TYPE)
193 .body(body.clone());
194 if let Some(token) = &self.inner.token {
195 request = request.bearer_auth(token);
196 }
197 let response = request.send().await?;
198 let status = response.status();
199 if status == StatusCode::TOO_MANY_REQUESTS {
200 let wait = retry_after(&response);
201 if attempt >= self.inner.max_retries {
202 return Err(SlackError::RateLimited { retry_after: wait });
203 }
204 attempt += 1;
205 tokio::time::sleep(wait.unwrap_or(DEFAULT_RETRY_AFTER)).await;
206 continue;
207 }
208 return Ok((status, response.bytes().await?));
209 }
210 }
211}
212
213#[derive(Default)]
215pub struct SlackClientBuilder {
216 http: Option<reqwest::Client>,
217 token: Option<String>,
218 base_url: Option<String>,
219 max_retries: Option<u32>,
220}
221
222impl SlackClientBuilder {
223 pub fn token(mut self, token: impl Into<String>) -> Self {
224 self.token = Some(token.into());
225 self
226 }
227
228 pub fn http_client(mut self, http: reqwest::Client) -> Self {
233 self.http = Some(http);
234 self
235 }
236
237 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
240 let mut base_url = base_url.into();
241 if !base_url.ends_with('/') {
242 base_url.push('/');
243 }
244 self.base_url = Some(base_url);
245 self
246 }
247
248 pub fn max_retries(mut self, max_retries: u32) -> Self {
254 self.max_retries = Some(max_retries);
255 self
256 }
257
258 pub fn build(self) -> SlackClient {
265 let http = self.http.unwrap_or_else(|| {
266 reqwest::Client::builder()
267 .timeout(DEFAULT_TIMEOUT)
268 .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
269 .build()
270 .expect("failed to initialize the TLS backend for reqwest")
271 });
272 SlackClient {
273 inner: Arc::new(Inner {
274 http,
275 token: self.token,
276 base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_owned()),
277 max_retries: self.max_retries.unwrap_or(DEFAULT_MAX_RETRIES),
278 }),
279 }
280 }
281}
282
283pub struct Pages<'a, M> {
285 client: &'a SlackClient,
286 request: Option<M>,
288}
289
290impl<M: CursorPaginated> Pages<'_, M> {
291 pub async fn next_page(&mut self) -> Option<Result<M::Response, SlackError>> {
293 let mut request = self.request.take()?;
294 let result = self.client.call(&request).await;
295 if let Ok(response) = &result {
296 if let Some(cursor) = response.next_cursor().filter(|c| !c.is_empty()) {
297 request.set_cursor(cursor.to_owned());
298 self.request = Some(request);
299 }
300 }
301 Some(result)
302 }
303}
304
305#[derive(Deserialize)]
307struct Envelope {
308 ok: bool,
309 #[serde(default)]
310 error: Option<String>,
311 #[serde(default)]
312 warning: Option<String>,
313 #[serde(default)]
314 response_metadata: Option<crate::ResponseMetadata>,
315}
316
317fn decode<R: DeserializeOwned>(status: StatusCode, body: &[u8]) -> Result<R, SlackError> {
318 let envelope: Envelope = match serde_json::from_slice(body) {
319 Ok(envelope) => envelope,
320 Err(source) => {
321 let body = String::from_utf8_lossy(body).into_owned();
322 if !status.is_success() {
323 return Err(SlackError::Http {
324 status: status.as_u16(),
325 body,
326 });
327 }
328 return Err(SlackError::Decode { source, body });
329 }
330 };
331 if !envelope.ok {
332 return Err(SlackError::Api(Box::new(SlackApiError {
333 error: envelope.error.unwrap_or_default(),
334 warning: envelope.warning,
335 response_metadata: envelope.response_metadata,
336 })));
337 }
338 serde_json::from_slice(body).map_err(|source| SlackError::Decode {
339 source,
340 body: String::from_utf8_lossy(body).into_owned(),
341 })
342}
343
344fn is_json(body: &[u8]) -> bool {
345 body.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{')
346}
347
348fn retry_after(response: &Response) -> Option<Duration> {
349 let secs = response
350 .headers()
351 .get(RETRY_AFTER)?
352 .to_str()
353 .ok()?
354 .trim()
355 .parse::<u64>()
356 .ok()?;
357 Some(Duration::from_secs(secs))
358}