Skip to main content

slack_web_api/
client.rs

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/";
11// 既定では 429 を再送しない。待ってよい時間は呼び出し側にしか決められない
12const DEFAULT_MAX_RETRIES: u32 = 0;
13// 自前で作る HTTP クライアントの時間切れ。reqwest の既定は時間切れ無しで、Slack が応答しないと待ち続ける
14const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
15const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16// 429 に Retry-After が無いときの待ち時間
17const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(1);
18const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
19
20/// A request for one Web API method. Implemented by every generated `*Request` type.
21pub trait SlackApiMethod: Serialize {
22    /// Method name, such as `chat.postMessage`.
23    const METHOD: &'static str;
24    /// Type of a successful response.
25    type Response: DeserializeOwned;
26}
27
28/// A request that supports cursor-based pagination.
29pub trait CursorPaginated: SlackApiMethod<Response: NextCursor> {
30    fn set_cursor(&mut self, cursor: String);
31}
32
33/// A response that carries the cursor for the next page.
34pub trait NextCursor {
35    /// Cursor for the next page, or `None` on the last page.
36    fn next_cursor(&self) -> Option<&str>;
37}
38
39/// Slack Web API client.
40///
41/// Clones share the underlying HTTP client and its connection pool, so cloning is cheap.
42/// Create one per application. To call with another token, use [`SlackClient::with_token`].
43///
44/// Every Web API method has a function here named after it (`chat.postMessage` is
45/// [`SlackClient::chat_post_message`]); see [`crate::api`] for the full list.
46///
47/// ```no_run
48/// use slack_web_api::api::UsersInfoRequest;
49/// use slack_web_api::SlackClient;
50///
51/// # async fn run() -> Result<(), slack_web_api::SlackError> {
52/// let client = SlackClient::new(std::env::var("SLACK_BOT_TOKEN").unwrap());
53/// let user = client.users_info(&UsersInfoRequest::new().user("U0123456789")).await?;
54/// println!("{:?}", user.user.and_then(|u| u.real_name));
55/// # Ok(())
56/// # }
57/// ```
58#[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    /// Creates a client that authenticates with `token` (`xoxb-`, `xoxp-`, ...), with a 30 second
82    /// request timeout and no retry on HTTP 429.
83    ///
84    /// # Panics
85    ///
86    /// When the TLS backend cannot be initialized, like `reqwest::Client::new()`.
87    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    /// Returns a client that uses another token but shares this client's connection pool and settings.
96    /// Useful for apps installed in many workspaces.
97    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    /// Calls a method with a typed request. The per-method functions such as `chat_post_message` call this.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`SlackError::Api`] when Slack answers `"ok": false`, [`SlackError::RateLimited`] when
113    /// 429 persists past the retry limit, and transport, HTTP or decode errors otherwise.
114    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    /// Calls a method by name with arbitrary parameters and returns the raw JSON.
120    /// Use it for methods or arguments that the generated types do not cover yet.
121    ///
122    /// # Errors
123    ///
124    /// Same as [`SlackClient::call`], plus [`SlackError::Encode`] when `params` is not a struct or map.
125    ///
126    /// ```no_run
127    /// # async fn run(client: slack_web_api::SlackClient) -> Result<(), slack_web_api::SlackError> {
128    /// let value = client
129    ///     .call_raw("chat.postMessage", &serde_json::json!({"channel": "C0123456789", "text": "hi"}))
130    ///     .await?;
131    /// println!("{}", value["ts"]);
132    /// # Ok(())
133    /// # }
134    /// ```
135    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    /// Iterates over the pages of a cursor-paginated method.
145    /// Drive it with `while let Some(page) = pages.next_page().await`.
146    pub fn pages<M: CursorPaginated>(&self, request: M) -> Pages<'_, M> {
147        Pages {
148            client: self,
149            request: Some(request),
150        }
151    }
152
153    // JSON ではなくファイルそのものを返すメソッド用(`admin.analytics.getFile`)。
154    // 失敗時は他のメソッドと同じく JSON のエラーが返る
155    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    // フォーム本文で POST し、429 なら Retry-After だけ待って再送する
178    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/// Builder for [`SlackClient`].
214#[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    /// Uses an existing `reqwest::Client` so the application and Slack share one connection pool.
229    /// Its own settings (timeouts, proxy, ...) are used as they are.
230    ///
231    /// Without it, the client is built with a 30 second request timeout and a 10 second connect timeout.
232    pub fn http_client(mut self, http: reqwest::Client) -> Self {
233        self.http = Some(http);
234        self
235    }
236
237    /// Base URL of the API (default `https://slack.com/api/`). Set it for GovSlack
238    /// (`https://slack-gov.com/api/`) or for a mock server in tests.
239    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    /// How many times a request is resent after a 429, waiting for `Retry-After` each time.
249    ///
250    /// The default is `0`: a 429 is returned at once as [`SlackError::RateLimited`] with the
251    /// `Retry-After` value, because only the caller knows how long a request may wait.
252    /// Other failures (5xx, network errors) are never retried, to avoid posting twice.
253    pub fn max_retries(mut self, max_retries: u32) -> Self {
254        self.max_retries = Some(max_retries);
255        self
256    }
257
258    /// Builds the client.
259    ///
260    /// # Panics
261    ///
262    /// When no `http_client` was given and the TLS backend cannot be initialized, like
263    /// `reqwest::Client::new()`.
264    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
283/// Cursor pagination over a method. Created by [`SlackClient::pages`].
284pub struct Pages<'a, M> {
285    client: &'a SlackClient,
286    // 次に送るリクエスト。最後のページを受け取ったら None
287    request: Option<M>,
288}
289
290impl<M: CursorPaginated> Pages<'_, M> {
291    /// Fetches the next page. Returns `None` after the last page and after an error.
292    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// 応答の `ok` と失敗時の詳細だけを先に読む(他の項目は読み飛ばす)
306#[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}