Skip to main content

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_util::future::BoxFuture;
12use futures_util::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    /// Validation methods (`apps.manifest.validate`, `blocks.validate`) return
183    /// `ok: false` with structured objects here, so the entries are kept raw;
184    /// see [`SlackEnvelopeMessage::errors_as_strings`] for a flat view.
185    pub errors: Option<Vec<serde_json::Value>>,
186    pub warnings: Option<Vec<String>>,
187}
188
189impl SlackEnvelopeMessage {
190    /// `errors` flattened for logging: strings verbatim, anything else as its
191    /// JSON text.
192    pub fn errors_as_strings(&self) -> Option<Vec<String>> {
193        self.errors.as_ref().map(|errors| {
194            errors
195                .iter()
196                .map(|value| match value {
197                    serde_json::Value::String(s) => s.clone(),
198                    other => other.to_string(),
199                })
200                .collect()
201        })
202    }
203}
204
205lazy_static! {
206    pub static ref SLACK_HTTP_EMPTY_GET_PARAMS: Vec<(&'static str, Option<&'static String>)> =
207        vec![];
208}
209
210impl<SCHC> SlackClientHttpApi<SCHC>
211where
212    SCHC: SlackClientHttpConnector + Send + Sync,
213{
214    fn new(http_connector: Arc<SCHC>) -> Self {
215        Self {
216            connector: http_connector,
217        }
218    }
219}
220
221pub struct SlackClientHttpApiUri;
222
223impl SlackClientHttpApiUri {
224    pub const SLACK_API_URI_STR: &'static str = "https://slack.com/api";
225
226    pub fn create_method_uri_path(method_relative_uri: &str) -> String {
227        format!("{}/{}", Self::SLACK_API_URI_STR, method_relative_uri)
228    }
229
230    pub fn create_url_with_params<'p, PT, TS>(base_url: Url, params: &'p PT) -> ClientResult<Url>
231    where
232        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
233        TS: AsRef<str> + 'p,
234    {
235        let url_query_params: Vec<(String, String)> = params
236            .clone()
237            .into_iter()
238            .filter_map(|(k, vo)| vo.map(|v| (k.to_string(), v.as_ref().to_string())))
239            .collect();
240
241        Ok(Url::parse_with_params(base_url.as_str(), url_query_params)?)
242    }
243}
244
245impl<SCHC> SlackClient<SCHC>
246where
247    SCHC: SlackClientHttpConnector + Send + Sync,
248{
249    pub fn new(http_connector: SCHC) -> Self {
250        Self {
251            http_api: SlackClientHttpApi::new(Arc::new(http_connector)),
252        }
253    }
254
255    pub fn open_session<'a>(&'a self, token: &'a SlackApiToken) -> SlackClientSession<'a, SCHC> {
256        let http_session_span = span!(
257            Level::DEBUG,
258            "Slack API request",
259            "/slack/team_id" = token
260                .team_id
261                .as_ref()
262                .map(|team_id| team_id.value().as_str())
263                .unwrap_or_else(|| "-")
264        );
265
266        let http_session_api = SlackClientHttpSessionApi {
267            client: self,
268            token,
269            span: http_session_span,
270        };
271
272        SlackClientSession { http_session_api }
273    }
274
275    pub async fn run_in_session<'a, FN, F, T>(&'a self, token: &'a SlackApiToken, pred: FN) -> T
276    where
277        FN: Fn(SlackClientSession<'a, SCHC>) -> F,
278        F: Future<Output = T>,
279    {
280        let session = self.open_session(token);
281        pred(session).await
282    }
283}
284
285impl<'a, SCHC> SlackClientHttpSessionApi<'a, SCHC>
286where
287    SCHC: SlackClientHttpConnector + Send,
288{
289    pub async fn http_get_uri<RS, PT, TS>(
290        &self,
291        full_uri: Url,
292        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
293    ) -> ClientResult<RS>
294    where
295        RS: for<'de> serde::de::Deserialize<'de> + Send,
296    {
297        let context = SlackClientApiCallContext {
298            rate_control_params,
299            token: Some(self.token),
300            tracing_span: &self.span,
301            is_sensitive_url: false,
302        };
303
304        self.client
305            .http_api
306            .connector
307            .http_get_uri(full_uri, context)
308            .await
309    }
310
311    pub async fn http_get<'p, RS, PT, TS>(
312        &self,
313        method_relative_uri: &str,
314        params: &'p PT,
315        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
316    ) -> ClientResult<RS>
317    where
318        RS: for<'de> serde::de::Deserialize<'de> + Send,
319        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
320        TS: AsRef<str> + 'p + Send,
321    {
322        let context = SlackClientApiCallContext {
323            rate_control_params,
324            token: Some(self.token),
325            tracing_span: &self.span,
326            is_sensitive_url: false,
327        };
328
329        self.client
330            .http_api
331            .connector
332            .http_get(method_relative_uri, params, context)
333            .await
334    }
335
336    pub async fn http_post<RQ, RS>(
337        &self,
338        method_relative_uri: &str,
339        request: &RQ,
340        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
341    ) -> ClientResult<RS>
342    where
343        RQ: serde::ser::Serialize + Send + Sync,
344        RS: for<'de> serde::de::Deserialize<'de> + Send,
345    {
346        let context = SlackClientApiCallContext {
347            rate_control_params,
348            token: Some(self.token),
349            tracing_span: &self.span,
350            is_sensitive_url: false,
351        };
352
353        self.client
354            .http_api
355            .connector
356            .http_post(method_relative_uri, &request, context)
357            .await
358    }
359
360    pub async fn http_post_uri<RQ, RS>(
361        &self,
362        full_uri: Url,
363        request: &RQ,
364        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
365    ) -> ClientResult<RS>
366    where
367        RQ: serde::ser::Serialize + Send + Sync,
368        RS: for<'de> serde::de::Deserialize<'de> + Send,
369    {
370        let context = SlackClientApiCallContext {
371            rate_control_params,
372            token: Some(self.token),
373            tracing_span: &self.span,
374            is_sensitive_url: false,
375        };
376
377        self.client
378            .http_api
379            .connector
380            .http_post_uri(full_uri, &request, context)
381            .await
382    }
383
384    pub async fn http_post_multipart_form<'p, RS, PT, TS>(
385        &self,
386        method_relative_uri: &str,
387        file: Option<FileMultipartData<'p>>,
388        params: &'p PT,
389        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
390    ) -> ClientResult<RS>
391    where
392        RS: for<'de> serde::de::Deserialize<'de> + Send,
393        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
394        TS: AsRef<str> + 'p + Send,
395    {
396        let context = SlackClientApiCallContext {
397            rate_control_params,
398            token: Some(self.token),
399            tracing_span: &self.span,
400            is_sensitive_url: false,
401        };
402
403        self.client
404            .http_api
405            .connector
406            .http_post_multipart_form(method_relative_uri, file, params, context)
407            .await
408    }
409
410    pub async fn http_post_uri_multipart_form<'p, RS, PT, TS>(
411        &self,
412        full_uri: Url,
413        file: Option<FileMultipartData<'p>>,
414        params: &'p PT,
415        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
416    ) -> ClientResult<RS>
417    where
418        RS: for<'de> serde::de::Deserialize<'de> + Send,
419        PT: std::iter::IntoIterator<Item = (&'p str, Option<TS>)> + Clone,
420        TS: AsRef<str> + 'p + Send,
421    {
422        let context = SlackClientApiCallContext {
423            rate_control_params,
424            token: Some(self.token),
425            tracing_span: &self.span,
426            is_sensitive_url: false,
427        };
428
429        self.client
430            .http_api
431            .connector
432            .http_post_uri_multipart_form(full_uri, file, params, context)
433            .await
434    }
435
436    pub async fn http_post_uri_binary<'p, RS>(
437        &self,
438        full_uri: Url,
439        content_type: String,
440        data: &'a [u8],
441        rate_control_params: Option<&'a SlackApiMethodRateControlConfig>,
442    ) -> ClientResult<RS>
443    where
444        RS: for<'de> serde::de::Deserialize<'de> + Send,
445    {
446        let context = SlackClientApiCallContext {
447            rate_control_params,
448            token: Some(self.token),
449            tracing_span: &self.span,
450            is_sensitive_url: true,
451        };
452
453        self.client
454            .http_api
455            .connector
456            .http_post_uri_binary(full_uri, content_type, data, context)
457            .await
458    }
459}