Skip to main content

slack_morphism/api/
files.rs

1//!
2//! Support for Slack Files API methods
3//!
4
5use crate::api::{
6    SlackApiUsersConversationsRequest, SlackApiUsersConversationsResponse,
7    SlackApiUsersProfileSetRequest, SlackApiUsersProfileSetResponse,
8};
9use crate::blocks::*;
10use crate::models::*;
11use crate::multipart_form::FileMultipartData;
12use crate::ratectl::*;
13use crate::{ClientResult, SlackClientHttpConnector};
14use crate::{SlackApiScrollableRequest, SlackApiScrollableResponse, SlackClientSession};
15use futures_util::future::BoxFuture;
16use futures_util::FutureExt;
17use rsb_derive::Builder;
18use rvstruct::ValueStruct;
19use serde::{Deserialize, Serialize, Serializer};
20use serde_with::skip_serializing_none;
21use url::Url;
22
23impl<'a, SCHC> SlackClientSession<'a, SCHC>
24where
25    SCHC: SlackClientHttpConnector + Send,
26{
27    ///
28    /// https://api.slack.com/methods/files.info
29    ///
30    pub async fn files_info(
31        &self,
32        req: &SlackApiFilesInfoRequest,
33    ) -> ClientResult<SlackApiFilesInfoResponse> {
34        self.http_session_api
35            .http_get(
36                "files.info",
37                &vec![("file", Some(req.file.value()))],
38                Some(&SLACK_TIER4_METHOD_CONFIG),
39            )
40            .await
41    }
42
43    ///
44    /// https://api.slack.com/methods/files.list
45    ///
46    pub async fn files_list(
47        &self,
48        req: &SlackApiFilesListRequest,
49    ) -> ClientResult<SlackApiFilesListResponse> {
50        self.http_session_api
51            .http_get(
52                "files.list",
53                &vec![
54                    ("channel", req.channel.as_ref().map(|x| x.value())),
55                    ("user", req.user.as_ref().map(|x| x.value())),
56                    ("types", req.types.as_ref()),
57                    ("count", req.count.map(|x| x.to_string()).as_ref()),
58                    ("page", req.page.map(|x| x.to_string()).as_ref()),
59                    ("ts_from", req.ts_from.map(|x| x.to_string()).as_ref()),
60                    ("ts_to", req.ts_to.map(|x| x.to_string()).as_ref()),
61                    (
62                        "show_files_hidden_by_limit",
63                        req.show_files_hidden_by_limit
64                            .map(|x| x.to_string())
65                            .as_ref(),
66                    ),
67                ],
68                Some(&SLACK_TIER3_METHOD_CONFIG),
69            )
70            .await
71    }
72
73    ///
74    /// https://api.slack.com/methods/files.upload
75    ///
76    #[deprecated(
77        note = "Deprecated by Slack. Use `getUploadURLExternal/files_upload_via_url/completeUploadExternal` instead."
78    )]
79    pub async fn files_upload(
80        &self,
81        req: &SlackApiFilesUploadRequest,
82    ) -> ClientResult<SlackApiFilesUploadResponse> {
83        let maybe_file = req.binary_content.as_ref().map(|file_data| {
84            let filename = req.filename.clone().unwrap_or("file".to_string());
85            let file_content_type = req.file_content_type.clone().unwrap_or_else(|| {
86                let file_mime = mime_guess::MimeGuess::from_path(&filename).first_or_octet_stream();
87                file_mime.to_string()
88            });
89            FileMultipartData {
90                name: filename,
91                content_type: file_content_type,
92                data: file_data.as_slice(),
93            }
94        });
95        self.http_session_api
96            .http_post_multipart_form(
97                "files.upload",
98                maybe_file,
99                &vec![
100                    (
101                        "channels",
102                        req.channels
103                            .as_ref()
104                            .map(|xs| {
105                                xs.iter()
106                                    .map(|x| x.to_string())
107                                    .collect::<Vec<String>>()
108                                    .join(",")
109                            })
110                            .as_ref(),
111                    ),
112                    ("content", req.content.as_ref()),
113                    ("filename", req.filename.as_ref()),
114                    ("filetype", req.filetype.as_ref().map(|x| x.value())),
115                    ("initial_comment", req.initial_comment.as_ref()),
116                    ("thread_ts", req.thread_ts.as_ref().map(|x| x.value())),
117                    ("title", req.title.as_ref()),
118                ],
119                Some(&SLACK_TIER2_METHOD_CONFIG),
120            )
121            .await
122    }
123
124    ///
125    /// https://api.slack.com/methods/files.getUploadURLExternal
126    ///
127    pub async fn get_upload_url_external(
128        &self,
129        req: &SlackApiFilesGetUploadUrlExternalRequest,
130    ) -> ClientResult<SlackApiFilesGetUploadUrlExternalResponse> {
131        self.http_session_api
132            .http_get(
133                "files.getUploadURLExternal",
134                &vec![
135                    ("filename", Some(&req.filename)),
136                    ("length", Some(&req.length.to_string())),
137                    ("alt_txt", req.alt_txt.as_ref()),
138                    ("snippet_type", req.snippet_type.as_ref().map(|v| v.value())),
139                ],
140                Some(&SLACK_TIER4_METHOD_CONFIG),
141            )
142            .await
143    }
144
145    pub async fn files_upload_via_url(
146        &self,
147        req: &SlackApiFilesUploadViaUrlRequest,
148    ) -> ClientResult<SlackApiFilesUploadViaUrlResponse> {
149        self.http_session_api
150            .http_post_uri_binary(
151                req.upload_url.value().clone(),
152                req.content_type.clone(),
153                &req.content,
154                Some(&SLACK_TIER4_METHOD_CONFIG),
155            )
156            .await
157    }
158
159    ///
160    /// https://api.slack.com/methods/files.completeUploadExternal
161    ///
162    pub async fn files_complete_upload_external(
163        &self,
164        req: &SlackApiFilesCompleteUploadExternalRequest,
165    ) -> ClientResult<SlackApiFilesCompleteUploadExternalResponse> {
166        self.http_session_api
167            .http_post(
168                "files.completeUploadExternal",
169                req,
170                Some(&SLACK_TIER4_METHOD_CONFIG),
171            )
172            .await
173    }
174
175    ///
176    /// https://api.slack.com/methods/files.delete
177    ///
178    pub async fn files_delete(
179        &self,
180        req: &SlackApiFilesDeleteRequest,
181    ) -> ClientResult<SlackApiFilesDeleteResponse> {
182        self.http_session_api
183            .http_post("files.delete", req, Some(&SLACK_TIER3_METHOD_CONFIG))
184            .await
185    }
186}
187
188#[skip_serializing_none]
189#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
190pub struct SlackApiFilesInfoRequest {
191    pub file: SlackFileId,
192}
193
194#[skip_serializing_none]
195#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
196pub struct SlackApiFilesInfoResponse {
197    pub file: SlackFile,
198}
199
200#[skip_serializing_none]
201#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
202pub struct SlackApiFilesListRequest {
203    pub channel: Option<SlackChannelId>,
204    pub user: Option<SlackUserId>,
205    pub types: Option<String>,
206    pub count: Option<u32>,
207    pub page: Option<u32>,
208    pub ts_from: Option<i64>,
209    pub ts_to: Option<i64>,
210    pub show_files_hidden_by_limit: Option<bool>,
211}
212
213#[skip_serializing_none]
214#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
215pub struct SlackApiFilesListResponse {
216    pub files: Vec<SlackFile>,
217    pub paging: Option<SlackApiFilesListPaging>,
218}
219
220#[skip_serializing_none]
221#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
222pub struct SlackApiFilesListPaging {
223    pub count: Option<u32>,
224    pub total: Option<u32>,
225    pub page: Option<u32>,
226    pub pages: Option<u32>,
227}
228
229impl<SCHC> SlackApiScrollableRequest<SCHC> for SlackApiFilesListRequest
230where
231    SCHC: SlackClientHttpConnector + Send + Sync + Clone + 'static,
232{
233    type ResponseType = SlackApiFilesListResponse;
234    type CursorType = u32;
235    type ResponseItemType = SlackFile;
236
237    fn with_new_cursor(&self, new_cursor: Option<&Self::CursorType>) -> Self {
238        self.clone().opt_page(new_cursor.cloned())
239    }
240
241    fn scroll<'a, 's>(
242        &'a self,
243        session: &'a SlackClientSession<'s, SCHC>,
244    ) -> BoxFuture<'a, ClientResult<Self::ResponseType>> {
245        async move { session.files_list(self).await }.boxed()
246    }
247}
248
249impl SlackApiScrollableResponse for SlackApiFilesListResponse {
250    type CursorType = u32;
251    type ResponseItemType = SlackFile;
252
253    fn next_cursor(&self) -> Option<Self::CursorType> {
254        self.paging
255            .as_ref()
256            .into_iter()
257            .filter_map(|paging| match (paging.page, paging.pages) {
258                (Some(page), Some(pages)) if page < pages => Some(page + 1),
259                _ => None,
260            })
261            .next()
262    }
263
264    fn scrollable_items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Self::ResponseItemType> + 'a> {
265        Box::new(self.files.iter())
266    }
267}
268
269#[skip_serializing_none]
270#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
271pub struct SlackApiFilesUploadRequest {
272    #[serde(serialize_with = "to_csv")]
273    pub channels: Option<Vec<SlackChannelId>>,
274    pub content: Option<String>,
275    pub binary_content: Option<Vec<u8>>,
276    pub filename: Option<String>,
277    pub filetype: Option<SlackFileType>,
278    pub initial_comment: Option<String>,
279    pub thread_ts: Option<SlackTs>,
280    pub title: Option<String>,
281    pub file_content_type: Option<String>,
282}
283
284#[skip_serializing_none]
285#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
286pub struct SlackApiFilesUploadResponse {
287    pub file: SlackFile,
288}
289
290#[skip_serializing_none]
291#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
292pub struct SlackApiFilesGetUploadUrlExternalRequest {
293    pub filename: String,
294    pub length: usize,
295    pub alt_txt: Option<String>,
296    pub snippet_type: Option<SlackFileSnippetType>,
297}
298
299#[skip_serializing_none]
300#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
301pub struct SlackApiFilesGetUploadUrlExternalResponse {
302    pub upload_url: SlackFileUploadUrl,
303    pub file_id: SlackFileId,
304}
305
306#[skip_serializing_none]
307#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
308pub struct SlackApiFilesUploadViaUrlRequest {
309    pub upload_url: SlackFileUploadUrl,
310    pub content: Vec<u8>,
311    pub content_type: String,
312}
313
314#[skip_serializing_none]
315#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
316pub struct SlackApiFilesUploadViaUrlResponse {}
317
318///
319/// https://api.slack.com/methods/files.completeUploadExternal
320///
321/// `initial_comment` and `blocks` are mutually exclusive: when `initial_comment` is set,
322/// Slack ignores `blocks`.
323///
324#[skip_serializing_none]
325#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
326pub struct SlackApiFilesCompleteUploadExternalRequest {
327    pub files: Vec<SlackApiFilesComplete>,
328    pub channel_id: Option<SlackChannelId>,
329    #[serde(serialize_with = "to_csv")]
330    pub channels: Option<Vec<SlackChannelId>>,
331    pub initial_comment: Option<String>,
332    /// Blocks for the file share message.
333    ///
334    /// Note: as of September 2026 Slack answers `internal_error` when this contains a
335    /// `markdown` block, in every request encoding, while the same block is accepted by
336    /// `chat.postMessage`. `section` and `rich_text` blocks work. Tracked upstream in
337    /// https://github.com/slackapi/python-slack-sdk/issues/1756
338    pub blocks: Option<Vec<SlackBlock>>,
339    pub thread_ts: Option<SlackTs>,
340    pub username: Option<String>,
341    pub icon_url: Option<Url>,
342    pub icon_emoji: Option<SlackEmoji>,
343}
344
345#[skip_serializing_none]
346#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
347pub struct SlackApiFilesCompleteUploadExternalResponse {
348    pub files: Vec<SlackFile>,
349}
350
351#[skip_serializing_none]
352#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
353pub struct SlackApiFilesComplete {
354    pub id: SlackFileId,
355    pub title: Option<String>,
356}
357
358#[skip_serializing_none]
359#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
360pub struct SlackApiFilesDeleteRequest {
361    pub file: SlackFileId,
362}
363
364#[skip_serializing_none]
365#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
366pub struct SlackApiFilesDeleteResponse {}
367
368fn to_csv<S: Serializer>(x: &Option<Vec<SlackChannelId>>, s: S) -> Result<S::Ok, S::Error> {
369    match x {
370        None => s.serialize_none(),
371        Some(ids) => {
372            let y: Vec<String> = ids.iter().map(|v| v.0.clone()).collect();
373            y.join(",").serialize(s)
374        }
375    }
376}
377
378#[cfg(test)]
379mod test {
380    use super::*;
381
382    #[test]
383    fn test_slack_api_files_complete_upload_external_request_serialization() {
384        let payload =
385            include_str!("./fixtures/slack_api_files_complete_upload_external_request.json");
386        let expected: serde_json::Value = serde_json::from_str(payload).unwrap();
387
388        let request =
389            SlackApiFilesCompleteUploadExternalRequest::new(vec![SlackApiFilesComplete::new(
390                SlackFileId("F123456".into()),
391            )])
392            .opt_channel_id(Some(SlackChannelId("C123456".into())))
393            .with_channels(vec![
394                SlackChannelId("C1".into()),
395                SlackChannelId("C2".into()),
396            ])
397            .with_blocks(vec![SlackMarkdownBlock::new("*bold* text".into()).into()])
398            .with_thread_ts(SlackTs("1234567890.123456".into()))
399            .with_username("test-bot".into())
400            .with_icon_url(Url::parse("https://example.com/icon.png").unwrap())
401            .with_icon_emoji(SlackEmoji::new(":tada:".into()));
402
403        let actual = serde_json::to_value(&request).unwrap();
404        assert_eq!(actual, expected);
405    }
406
407    #[test]
408    fn test_slack_api_files_complete_upload_external_request_minimal() {
409        let request =
410            SlackApiFilesCompleteUploadExternalRequest::new(vec![SlackApiFilesComplete::new(
411                SlackFileId("F123456".into()),
412            )]);
413
414        let actual = serde_json::to_value(&request).unwrap();
415        let expected: serde_json::Value =
416            serde_json::from_str(r#"{"files":[{"id":"F123456"}]}"#).unwrap();
417        assert_eq!(actual, expected);
418    }
419
420    #[test]
421    fn test_slack_api_files_complete_upload_external_response() {
422        let payload =
423            include_str!("./fixtures/slack_api_files_complete_upload_external_response.json");
424        let model: SlackApiFilesCompleteUploadExternalResponse =
425            serde_json::from_str(payload).unwrap();
426
427        assert_eq!(model.files.len(), 1);
428        assert_eq!(model.files[0].id, SlackFileId("F123456".into()));
429        assert_eq!(model.files[0].title, Some("test-file".into()));
430    }
431}