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 pub errors: Option<Vec<serde_json::Value>>,
186 pub warnings: Option<Vec<String>>,
187}
188
189impl SlackEnvelopeMessage {
190 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}