slack_morphism/
client.rs

1use serde::{Deserialize, Serialize};
2use std::future::Future;
3use std::sync::Arc;
4
5use crate::token::*;
6
7use crate::errors::SlackClientError;
8use crate::models::*;
9use crate::multipart_form::FileMultipartData;
10use crate::ratectl::SlackApiMethodRateControlConfig;
11use futures::future::BoxFuture;
12use futures::FutureExt;
13use lazy_static::*;
14use rvstruct::ValueStruct;
15use tracing::*;
16use url::Url;
17
18#[derive(Clone, Debug)]
19pub struct SlackClient<SCHC>
20where
21    SCHC: SlackClientHttpConnector + Send,
22{
23    pub http_api: SlackClientHttpApi<SCHC>,
24}
25
26#[derive(Clone, Debug)]
27pub struct SlackClientHttpApi<SCHC>
28where
29    SCHC: SlackClientHttpConnector + Send,
30{
31    pub connector: Arc<SCHC>,
32}
33
34#[derive(Debug)]
35pub struct SlackClientSession<'a, SCHC>
36where
37    SCHC: SlackClientHttpConnector + Send,
38{
39    pub http_session_api: SlackClientHttpSessionApi<'a, SCHC>,
40}
41
42#[derive(Debug)]
43pub struct SlackClientHttpSessionApi<'a, SCHC>
44where
45    SCHC: SlackClientHttpConnector + Send,
46{
47    pub client: &'a SlackClient<SCHC>,
48    token: &'a SlackApiToken,
49    pub span: Span,
50}
51
52#[derive(Debug, Clone)]
53pub struct SlackClientApiCallContext<'a> {
54    pub rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
55    pub token: Option<&'a SlackApiToken>,
56    pub tracing_span: &'a Span,
57    pub is_sensitive_url: bool,
58}
59
60pub trait SlackClientHttpConnector {
61    fn http_get_uri<'a, RS>(
62        &'a self,
63        full_uri: Url,
64        context: SlackClientApiCallContext<'a>,
65    ) -> BoxFuture<'a, ClientResult<RS>>
66    where
67        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + 'a + Send;
68
69    fn http_get_with_client_secret<'a, RS>(
70        &'a self,
71        full_uri: Url,
72        client_id: &'a SlackClientId,
73        client_secret: &'a SlackClientSecret,
74    ) -> BoxFuture<'a, ClientResult<RS>>
75    where
76        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + 'a + Send;
77
78    fn http_get<'a, 'p, RS, PT, TS>(
79        &'a self,
80        method_relative_uri: &str,
81        params: &'p PT,
82        context: SlackClientApiCallContext<'a>,
83    ) -> BoxFuture<'a, ClientResult<RS>>
84    where
85        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a,
86        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
87        TS: AsRef<str> + 'p + Send,
88    {
89        let full_uri = self
90            .create_method_uri_path(method_relative_uri)
91            .and_then(|url| SlackClientHttpApiUri::create_url_with_params(url, params));
92
93        match full_uri {
94            Ok(full_uri) => self.http_get_uri(full_uri, context),
95            Err(err) => std::future::ready(Err(err)).boxed(),
96        }
97    }
98
99    fn http_post_uri<'a, RQ, RS>(
100        &'a self,
101        full_uri: Url,
102        request_body: &'a RQ,
103        context: SlackClientApiCallContext<'a>,
104    ) -> BoxFuture<'a, ClientResult<RS>>
105    where
106        RQ: serde::ser::Serialize + Send + Sync,
107        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + Send + 'a;
108
109    fn http_post<'a, RQ, RS>(
110        &'a self,
111        method_relative_uri: &str,
112        request: &'a RQ,
113        context: SlackClientApiCallContext<'a>,
114    ) -> BoxFuture<'a, ClientResult<RS>>
115    where
116        RQ: serde::ser::Serialize + Send + Sync,
117        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a,
118    {
119        match self.create_method_uri_path(method_relative_uri) {
120            Ok(full_uri) => self.http_post_uri(full_uri, request, context),
121            Err(err) => std::future::ready(Err(err)).boxed(),
122        }
123    }
124
125    fn http_post_uri_multipart_form<'a, 'p, RS, PT, TS>(
126        &'a self,
127        full_uri: Url,
128        file: Option<FileMultipartData<'p>>,
129        params: &'p PT,
130        context: SlackClientApiCallContext<'a>,
131    ) -> BoxFuture<'a, ClientResult<RS>>
132    where
133        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + Send + 'a,
134        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
135        TS: AsRef<str> + 'p + Send;
136
137    fn http_post_multipart_form<'a, 'p, RS, PT, TS>(
138        &'a self,
139        method_relative_uri: &str,
140        file: Option<FileMultipartData<'p>>,
141        params: &'p PT,
142        context: SlackClientApiCallContext<'a>,
143    ) -> BoxFuture<'a, ClientResult<RS>>
144    where
145        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a,
146        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
147        TS: AsRef<str> + 'p + Send,
148    {
149        match self.create_method_uri_path(method_relative_uri) {
150            Ok(full_uri) => self.http_post_uri_multipart_form(full_uri, file, params, context),
151            Err(err) => std::future::ready(Err(err)).boxed(),
152        }
153    }
154
155    fn http_post_uri_binary<'a, 'p, RS>(
156        &'a self,
157        full_uri: Url,
158        content_type: String,
159        data: &'a [u8],
160        context: SlackClientApiCallContext<'a>,
161    ) -> BoxFuture<'a, ClientResult<RS>>
162    where
163        RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + Send + 'a;
164
165    fn create_method_uri_path(&self, method_relative_uri: &str) -> ClientResult<Url> {
166        Ok(SlackClientHttpApiUri::create_method_uri_path(method_relative_uri).parse()?)
167    }
168}
169
170pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
171
172pub type UserCallbackResult<T> = std::result::Result<T, BoxError>;
173
174pub type ClientResult<T> = std::result::Result<T, SlackClientError>;
175
176pub type AnyStdResult<T> = std::result::Result<T, BoxError>;
177
178#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
179pub struct SlackEnvelopeMessage {
180    pub ok: bool,
181    pub error: Option<String>,
182    // Slack may return validation errors in `errors` field with `ok: false` for some methods (such as `apps.manifest.validate`.
183    pub errors: Option<Vec<String>>,
184    pub warnings: Option<Vec<String>>,
185}
186
187lazy_static! {
188    pub static ref SLACK_HTTP_EMPTY_GET_PARAMS: Vec<(&'static str, Option<&'static String>)> =
189        vec![];
190}
191
192impl<SCHC> SlackClientHttpApi<SCHC>
193where
194    SCHC: SlackClientHttpConnector + Send + Sync,
195{
196    fn new(http_connector: Arc<SCHC>) -> Self {
197        Self {
198            connector: http_connector,
199        }
200    }
201}
202
203pub struct SlackClientHttpApiUri;
204
205impl SlackClientHttpApiUri {
206    pub const SLACK_API_URI_STR: &'static str = "https://slack.com/api";
207
208    pub fn create_method_uri_path(method_relative_uri: &str) -> String {
209        format!("{}/{}", Self::SLACK_API_URI_STR, method_relative_uri)
210    }
211
212    pub fn create_url_with_params<'p, PT, TS>(base_url: Url, params: &'p PT) -> ClientResult<Url>
213    where
214        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
215        TS: AsRef<str> + 'p,
216    {
217        let url_query_params: Vec<(String, String)> = params
218            .clone()
219            .into_iter()
220            .filter_map(|(k, vo)| vo.map(|v| (k.to_string(), v.as_ref().to_string())))
221            .collect();
222
223        Ok(Url::parse_with_params(base_url.as_str(), url_query_params)?)
224    }
225}
226
227impl<SCHC> SlackClient<SCHC>
228where
229    SCHC: SlackClientHttpConnector + Send + Sync,
230{
231    pub fn new(http_connector: SCHC) -> Self {
232        Self {
233            http_api: SlackClientHttpApi::new(Arc::new(http_connector)),
234        }
235    }
236
237    pub fn open_session<'a>(&'a self, token: &'a SlackApiToken) -> SlackClientSession<'a, SCHC> {
238        let http_session_span = span!(
239            Level::DEBUG,
240            "Slack API request",
241            "/slack/team_id" = token
242                .team_id
243                .as_ref()
244                .map(|team_id| team_id.value().as_str())
245                .unwrap_or_else(|| "-")
246        );
247
248        let http_session_api = SlackClientHttpSessionApi {
249            client: self,
250            token,
251            span: http_session_span,
252        };
253
254        SlackClientSession { http_session_api }
255    }
256
257    pub async fn run_in_session<'a, FN, F, T>(&'a self, token: &'a SlackApiToken, pred: FN) -> T
258    where
259        FN: Fn(SlackClientSession<'a, SCHC>) -> F,
260        F: Future<Output = T>,
261    {
262        let session = self.open_session(token);
263        pred(session).await
264    }
265}
266
267impl<'a, SCHC> SlackClientHttpSessionApi<'a, SCHC>
268where
269    SCHC: SlackClientHttpConnector + Send,
270{
271    pub async fn http_get_uri<RS, PT, TS>(
272        &self,
273        full_uri: Url,
274        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
275    ) -> ClientResult<RS>
276    where
277        RS: for<'de> serde::de::Deserialize<'de> + Send,
278    {
279        let context = SlackClientApiCallContext {
280            rate_control_params,
281            token: Some(self.token),
282            tracing_span: &self.span,
283            is_sensitive_url: false,
284        };
285
286        self.client
287            .http_api
288            .connector
289            .http_get_uri(full_uri, context)
290            .await
291    }
292
293    pub async fn http_get<'p, RS, PT, TS>(
294        &self,
295        method_relative_uri: &str,
296        params: &'p PT,
297        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
298    ) -> ClientResult<RS>
299    where
300        RS: for<'de> serde::de::Deserialize<'de> + Send,
301        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
302        TS: AsRef<str> + 'p + Send,
303    {
304        let context = SlackClientApiCallContext {
305            rate_control_params,
306            token: Some(self.token),
307            tracing_span: &self.span,
308            is_sensitive_url: false,
309        };
310
311        self.client
312            .http_api
313            .connector
314            .http_get(method_relative_uri, params, context)
315            .await
316    }
317
318    pub async fn http_post<RQ, RS>(
319        &self,
320        method_relative_uri: &str,
321        request: &RQ,
322        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
323    ) -> ClientResult<RS>
324    where
325        RQ: serde::ser::Serialize + Send + Sync,
326        RS: for<'de> serde::de::Deserialize<'de> + Send,
327    {
328        let context = SlackClientApiCallContext {
329            rate_control_params,
330            token: Some(self.token),
331            tracing_span: &self.span,
332            is_sensitive_url: false,
333        };
334
335        self.client
336            .http_api
337            .connector
338            .http_post(method_relative_uri, &request, context)
339            .await
340    }
341
342    pub async fn http_post_uri<RQ, RS>(
343        &self,
344        full_uri: Url,
345        request: &RQ,
346        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
347    ) -> ClientResult<RS>
348    where
349        RQ: serde::ser::Serialize + Send + Sync,
350        RS: for<'de> serde::de::Deserialize<'de> + Send,
351    {
352        let context = SlackClientApiCallContext {
353            rate_control_params,
354            token: Some(self.token),
355            tracing_span: &self.span,
356            is_sensitive_url: false,
357        };
358
359        self.client
360            .http_api
361            .connector
362            .http_post_uri(full_uri, &request, context)
363            .await
364    }
365
366    pub async fn http_post_multipart_form<'p, RS, PT, TS>(
367        &self,
368        method_relative_uri: &str,
369        file: Option<FileMultipartData<'p>>,
370        params: &'p PT,
371        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
372    ) -> ClientResult<RS>
373    where
374        RS: for<'de> serde::de::Deserialize<'de> + Send,
375        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
376        TS: AsRef<str> + 'p + Send,
377    {
378        let context = SlackClientApiCallContext {
379            rate_control_params,
380            token: Some(self.token),
381            tracing_span: &self.span,
382            is_sensitive_url: false,
383        };
384
385        self.client
386            .http_api
387            .connector
388            .http_post_multipart_form(method_relative_uri, file, params, context)
389            .await
390    }
391
392    pub async fn http_post_uri_multipart_form<'p, RS, PT, TS>(
393        &self,
394        full_uri: Url,
395        file: Option<FileMultipartData<'p>>,
396        params: &'p PT,
397        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
398    ) -> ClientResult<RS>
399    where
400        RS: for<'de> serde::de::Deserialize<'de> + Send,
401        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
402        TS: AsRef<str> + 'p + Send,
403    {
404        let context = SlackClientApiCallContext {
405            rate_control_params,
406            token: Some(self.token),
407            tracing_span: &self.span,
408            is_sensitive_url: false,
409        };
410
411        self.client
412            .http_api
413            .connector
414            .http_post_uri_multipart_form(full_uri, file, params, context)
415            .await
416    }
417
418    pub async fn http_post_uri_binary<'p, RS>(
419        &self,
420        full_uri: Url,
421        content_type: String,
422        data: &'a [u8],
423        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
424    ) -> ClientResult<RS>
425    where
426        RS: for<'de> serde::de::Deserialize<'de> + Send,
427    {
428        let context = SlackClientApiCallContext {
429            rate_control_params,
430            token: Some(self.token),
431            tracing_span: &self.span,
432            is_sensitive_url: true,
433        };
434
435        self.client
436            .http_api
437            .connector
438            .http_post_uri_binary(full_uri, content_type, data, context)
439            .await
440    }
441}