1use crate::api::{
10 FilesCompleteUploadExternalRequest, FilesCompleteUploadExternalResponse,
11 FilesGetUploadUrlExternalRequest,
12};
13use crate::blocks::Block;
14use crate::{SlackClient, SlackError};
15use bytes::Bytes;
16use serde_json::json;
17use tokio::task::JoinSet;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct FileUpload {
22 filename: String,
23 content: Bytes,
24 title: Option<String>,
25 alt_txt: Option<String>,
26 snippet_type: Option<String>,
27}
28
29impl FileUpload {
30 pub fn new(filename: impl Into<String>, content: impl Into<Bytes>) -> Self {
31 Self {
32 filename: filename.into(),
33 content: content.into(),
34 title: None,
35 alt_txt: None,
36 snippet_type: None,
37 }
38 }
39
40 pub fn title(mut self, title: impl Into<String>) -> Self {
42 self.title = Some(title.into());
43 self
44 }
45
46 pub fn alt_txt(mut self, alt_txt: impl Into<String>) -> Self {
48 self.alt_txt = Some(alt_txt.into());
49 self
50 }
51
52 pub fn snippet_type(mut self, snippet_type: impl Into<String>) -> Self {
54 self.snippet_type = Some(snippet_type.into());
55 self
56 }
57}
58
59#[derive(Debug, Clone, Default, PartialEq)]
61pub struct UploadDestination {
62 channel_id: Option<String>,
63 thread_ts: Option<String>,
64 initial_comment: Option<String>,
65 blocks: Option<Vec<Block>>,
66}
67
68impl UploadDestination {
69 pub fn channel(channel_id: impl Into<String>) -> Self {
70 Self {
71 channel_id: Some(channel_id.into()),
72 ..Self::default()
73 }
74 }
75
76 pub fn thread_ts(mut self, thread_ts: impl Into<String>) -> Self {
78 self.thread_ts = Some(thread_ts.into());
79 self
80 }
81
82 pub fn initial_comment(mut self, initial_comment: impl Into<String>) -> Self {
84 self.initial_comment = Some(initial_comment.into());
85 self
86 }
87
88 pub fn blocks(mut self, blocks: Vec<Block>) -> Self {
90 self.blocks = Some(blocks);
91 self
92 }
93}
94
95impl SlackClient {
96 pub async fn upload_files(
106 &self,
107 files: Vec<FileUpload>,
108 destination: UploadDestination,
109 ) -> Result<FilesCompleteUploadExternalResponse, SlackError> {
110 let mut tasks = JoinSet::new();
111 for (index, file) in files.into_iter().enumerate() {
112 let client = self.clone();
113 tasks.spawn(async move { client.upload_one(file).await.map(|entry| (index, entry)) });
114 }
115 let mut uploaded = Vec::with_capacity(tasks.len());
116 while let Some(joined) = tasks.join_next().await {
117 uploaded.push(joined.map_err(|e| SlackError::Task(e.to_string()))??);
118 }
119 uploaded.sort_by_key(|(index, _)| *index);
120
121 let mut request =
122 FilesCompleteUploadExternalRequest::new(uploaded.into_iter().map(|(_, e)| e).collect());
123 request.channel_id = destination.channel_id;
124 request.thread_ts = destination.thread_ts;
125 request.initial_comment = destination.initial_comment;
126 request.blocks = destination.blocks;
127 self.files_complete_upload_external(&request).await
128 }
129
130 async fn upload_one(&self, file: FileUpload) -> Result<serde_json::Value, SlackError> {
132 let mut request =
133 FilesGetUploadUrlExternalRequest::new(file.content.len() as i64, file.filename.clone());
134 request.alt_txt = file.alt_txt;
135 request.snippet_type = file.snippet_type;
136 let target = self.files_get_upload_url_external(&request).await?;
137 let (Some(upload_url), Some(file_id)) = (target.upload_url, target.file_id) else {
138 return Err(SlackError::MissingField(
139 "files.getUploadURLExternal: upload_url / file_id",
140 ));
141 };
142
143 let response = self
144 .http()
145 .post(&upload_url)
146 .body(file.content)
147 .send()
148 .await?;
149 let status = response.status();
150 if !status.is_success() {
151 let body = response.text().await.unwrap_or_default();
152 return Err(SlackError::Http {
153 status: status.as_u16(),
154 body,
155 });
156 }
157 Ok(json!({ "id": file_id, "title": file.title.unwrap_or(file.filename) }))
158 }
159}