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/";
11const DEFAULT_MAX_RETRIES: u32 = 3;
12// 429 に Retry-After が無いときの待ち時間
13const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(1);
14const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
15
16/// A request for one Web API method. Implemented by every generated `*Request` type.
17pub trait SlackApiMethod: Serialize {
18    /// Method name, such as `chat.postMessage`.
19    const METHOD: &'static str;
20    /// Type of a successful response.
21    type Response: DeserializeOwned;
22}
23
24/// A request that supports cursor-based pagination.
25pub trait CursorPaginated: SlackApiMethod<Response: NextCursor> {
26    fn set_cursor(&mut self, cursor: String);
27}
28
29/// A response that carries the cursor for the next page.
30pub trait NextCursor {
31    /// Cursor for the next page, or `None` on the last page.
32    fn next_cursor(&self) -> Option<&str>;
33}
34
35/// Slack Web API client.
36///
37/// Clones share the underlying HTTP client and its connection pool, so cloning is cheap.
38/// Create one per application. To call with another token, use [`SlackClient::with_token`].
39///
40/// Every Web API method has a function here named after it (`chat.postMessage` is
41/// [`SlackClient::chat_post_message`]); see [`crate::api`] for the full list.
42///
43/// ```no_run
44/// use slack_web_api::api::UsersInfoRequest;
45/// use slack_web_api::SlackClient;
46///
47/// # async fn run() -> Result<(), slack_web_api::SlackError> {
48/// let client = SlackClient::new(std::env::var("SLACK_BOT_TOKEN").unwrap());
49/// let user = client.users_info(&UsersInfoRequest::new().user("U0123456789")).await?;
50/// println!("{:?}", user.user.and_then(|u| u.real_name));
51/// # Ok(())
52/// # }
53/// ```
54#[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    /// Creates a client that authenticates with `token` (`xoxb-`, `xoxp-`, ...).
78    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    /// Returns a client that uses another token but shares this client's connection pool and settings.
87    /// Useful for apps installed in many workspaces.
88    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    /// Calls a method with a typed request. The per-method functions such as `chat_post_message` call this.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`SlackError::Api`] when Slack answers `"ok": false`, [`SlackError::RateLimited`] when
104    /// 429 persists past the retry limit, and transport, HTTP or decode errors otherwise.
105    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    /// Calls a method by name with arbitrary parameters and returns the raw JSON.
111    /// Use it for methods or arguments that the generated types do not cover yet.
112    ///
113    /// # Errors
114    ///
115    /// Same as [`SlackClient::call`], plus [`SlackError::Encode`] when `params` is not a struct or map.
116    ///
117    /// ```no_run
118    /// # async fn run(client: slack_web_api::SlackClient) -> Result<(), slack_web_api::SlackError> {
119    /// let value = client
120    ///     .call_raw("chat.postMessage", &serde_json::json!({"channel": "C0123456789", "text": "hi"}))
121    ///     .await?;
122    /// println!("{}", value["ts"]);
123    /// # Ok(())
124    /// # }
125    /// ```
126    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    /// Iterates over the pages of a cursor-paginated method.
136    /// Drive it with `while let Some(page) = pages.next_page().await`.
137    pub fn pages<M: CursorPaginated>(&self, request: M) -> Pages<'_, M> {
138        Pages {
139            client: self,
140            request: Some(request),
141        }
142    }
143
144    // JSON ではなくファイルそのものを返すメソッド用(`admin.analytics.getFile`)。
145    // 失敗時は他のメソッドと同じく JSON のエラーが返る
146    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    // フォーム本文で POST し、429 なら Retry-After だけ待って再送する
169    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/// Builder for [`SlackClient`].
205#[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    /// Uses an existing `reqwest::Client` so the application and Slack share one connection pool.
220    /// Defaults to `reqwest::Client::default()`.
221    pub fn http_client(mut self, http: reqwest::Client) -> Self {
222        self.http = Some(http);
223        self
224    }
225
226    /// Base URL of the API (default `https://slack.com/api/`). Set it for GovSlack
227    /// (`https://slack-gov.com/api/`) or for a mock server in tests.
228    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    /// How many times a request is resent after a 429, waiting for `Retry-After` each time
238    /// (default 3, `0` disables retries).
239    /// Other failures (5xx, network errors) are never retried, to avoid posting twice.
240    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
257/// Cursor pagination over a method. Created by [`SlackClient::pages`].
258pub struct Pages<'a, M> {
259    client: &'a SlackClient,
260    // 次に送るリクエスト。最後のページを受け取ったら None
261    request: Option<M>,
262}
263
264impl<M: CursorPaginated> Pages<'_, M> {
265    /// Fetches the next page. Returns `None` after the last page and after an error.
266    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// 応答の `ok` と失敗時の詳細だけを先に読む(他の項目は読み飛ばす)
280#[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}