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 = 3;
12const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(1);
14const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
15
16pub trait SlackApiMethod: Serialize {
18 const METHOD: &'static str;
20 type Response: DeserializeOwned;
22}
23
24pub trait CursorPaginated: SlackApiMethod<Response: NextCursor> {
26 fn set_cursor(&mut self, cursor: String);
27}
28
29pub trait NextCursor {
31 fn next_cursor(&self) -> Option<&str>;
33}
34
35#[derive(Clone)]
55pub struct SlackClient {
56 inner: Arc<Inner>,
57}
58
59struct Inner {
60 http: reqwest::Client,
61 token: Option<String>,
62 base_url: String,
63 max_retries: u32,
64}
65
66impl std::fmt::Debug for SlackClient {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_struct("SlackClient")
69 .field("token", &self.inner.token.as_ref().map(|_| "***"))
70 .field("base_url", &self.inner.base_url)
71 .field("max_retries", &self.inner.max_retries)
72 .finish()
73 }
74}
75
76impl SlackClient {
77 pub fn new(token: impl Into<String>) -> Self {
79 Self::builder().token(token).build()
80 }
81
82 pub fn builder() -> SlackClientBuilder {
83 SlackClientBuilder::default()
84 }
85
86 pub fn with_token(&self, token: impl Into<String>) -> Self {
89 Self {
90 inner: Arc::new(Inner {
91 http: self.inner.http.clone(),
92 token: Some(token.into()),
93 base_url: self.inner.base_url.clone(),
94 max_retries: self.inner.max_retries,
95 }),
96 }
97 }
98
99 pub async fn call<M: SlackApiMethod>(&self, request: &M) -> Result<M::Response, SlackError> {
106 let (status, body) = self.post(M::METHOD, request).await?;
107 decode(status, &body)
108 }
109
110 pub async fn call_raw<P: Serialize + ?Sized>(
127 &self,
128 method: &str,
129 params: &P,
130 ) -> Result<serde_json::Value, SlackError> {
131 let (status, body) = self.post(method, params).await?;
132 decode(status, &body)
133 }
134
135 pub fn pages<M: CursorPaginated>(&self, request: M) -> Pages<'_, M> {
138 Pages {
139 client: self,
140 request: Some(request),
141 }
142 }
143
144 pub(crate) async fn call_bytes<P: Serialize + ?Sized>(
147 &self,
148 method: &str,
149 params: &P,
150 ) -> Result<Bytes, SlackError> {
151 let (status, body) = self.post(method, params).await?;
152 if is_json(&body) {
153 decode::<serde::de::IgnoredAny>(status, &body)?;
154 }
155 if !status.is_success() {
156 return Err(SlackError::Http {
157 status: status.as_u16(),
158 body: String::from_utf8_lossy(&body).into_owned(),
159 });
160 }
161 Ok(body)
162 }
163
164 pub(crate) fn http(&self) -> &reqwest::Client {
165 &self.inner.http
166 }
167
168 async fn post<P: Serialize + ?Sized>(
170 &self,
171 method: &str,
172 params: &P,
173 ) -> Result<(StatusCode, Bytes), SlackError> {
174 let form = crate::form::to_form(params).map_err(|e| SlackError::Encode(e.to_string()))?;
175 let body = Bytes::from(form);
176 let url = format!("{}{}", self.inner.base_url, method);
177 let mut attempt = 0;
178 loop {
179 let mut request = self
180 .inner
181 .http
182 .post(&url)
183 .header(CONTENT_TYPE, FORM_CONTENT_TYPE)
184 .body(body.clone());
185 if let Some(token) = &self.inner.token {
186 request = request.bearer_auth(token);
187 }
188 let response = request.send().await?;
189 let status = response.status();
190 if status == StatusCode::TOO_MANY_REQUESTS {
191 let wait = retry_after(&response);
192 if attempt >= self.inner.max_retries {
193 return Err(SlackError::RateLimited { retry_after: wait });
194 }
195 attempt += 1;
196 tokio::time::sleep(wait.unwrap_or(DEFAULT_RETRY_AFTER)).await;
197 continue;
198 }
199 return Ok((status, response.bytes().await?));
200 }
201 }
202}
203
204#[derive(Default)]
206pub struct SlackClientBuilder {
207 http: Option<reqwest::Client>,
208 token: Option<String>,
209 base_url: Option<String>,
210 max_retries: Option<u32>,
211}
212
213impl SlackClientBuilder {
214 pub fn token(mut self, token: impl Into<String>) -> Self {
215 self.token = Some(token.into());
216 self
217 }
218
219 pub fn http_client(mut self, http: reqwest::Client) -> Self {
222 self.http = Some(http);
223 self
224 }
225
226 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
229 let mut base_url = base_url.into();
230 if !base_url.ends_with('/') {
231 base_url.push('/');
232 }
233 self.base_url = Some(base_url);
234 self
235 }
236
237 pub fn max_retries(mut self, max_retries: u32) -> Self {
241 self.max_retries = Some(max_retries);
242 self
243 }
244
245 pub fn build(self) -> SlackClient {
246 SlackClient {
247 inner: Arc::new(Inner {
248 http: self.http.unwrap_or_default(),
249 token: self.token,
250 base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_owned()),
251 max_retries: self.max_retries.unwrap_or(DEFAULT_MAX_RETRIES),
252 }),
253 }
254 }
255}
256
257pub struct Pages<'a, M> {
259 client: &'a SlackClient,
260 request: Option<M>,
262}
263
264impl<M: CursorPaginated> Pages<'_, M> {
265 pub async fn next_page(&mut self) -> Option<Result<M::Response, SlackError>> {
267 let mut request = self.request.take()?;
268 let result = self.client.call(&request).await;
269 if let Ok(response) = &result {
270 if let Some(cursor) = response.next_cursor().filter(|c| !c.is_empty()) {
271 request.set_cursor(cursor.to_owned());
272 self.request = Some(request);
273 }
274 }
275 Some(result)
276 }
277}
278
279#[derive(Deserialize)]
281struct Envelope {
282 ok: bool,
283 #[serde(default)]
284 error: Option<String>,
285 #[serde(default)]
286 warning: Option<String>,
287 #[serde(default)]
288 response_metadata: Option<crate::ResponseMetadata>,
289}
290
291fn decode<R: DeserializeOwned>(status: StatusCode, body: &[u8]) -> Result<R, SlackError> {
292 let envelope: Envelope = match serde_json::from_slice(body) {
293 Ok(envelope) => envelope,
294 Err(source) => {
295 let body = String::from_utf8_lossy(body).into_owned();
296 if !status.is_success() {
297 return Err(SlackError::Http {
298 status: status.as_u16(),
299 body,
300 });
301 }
302 return Err(SlackError::Decode { source, body });
303 }
304 };
305 if !envelope.ok {
306 return Err(SlackError::Api(Box::new(SlackApiError {
307 error: envelope.error.unwrap_or_default(),
308 warning: envelope.warning,
309 response_metadata: envelope.response_metadata,
310 })));
311 }
312 serde_json::from_slice(body).map_err(|source| SlackError::Decode {
313 source,
314 body: String::from_utf8_lossy(body).into_owned(),
315 })
316}
317
318fn is_json(body: &[u8]) -> bool {
319 body.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{')
320}
321
322fn retry_after(response: &Response) -> Option<Duration> {
323 let secs = response
324 .headers()
325 .get(RETRY_AFTER)?
326 .to_str()
327 .ok()?
328 .trim()
329 .parse::<u64>()
330 .ok()?;
331 Some(Duration::from_secs(secs))
332}