use crate::api::{
FilesCompleteUploadExternalRequest, FilesCompleteUploadExternalResponse,
FilesGetUploadUrlExternalRequest,
};
use crate::blocks::Block;
use crate::{SlackClient, SlackError};
use bytes::Bytes;
use serde_json::json;
use tokio::task::JoinSet;
#[derive(Debug, Clone, PartialEq)]
pub struct FileUpload {
filename: String,
content: Bytes,
title: Option<String>,
alt_txt: Option<String>,
snippet_type: Option<String>,
}
impl FileUpload {
pub fn new(filename: impl Into<String>, content: impl Into<Bytes>) -> Self {
Self {
filename: filename.into(),
content: content.into(),
title: None,
alt_txt: None,
snippet_type: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn alt_txt(mut self, alt_txt: impl Into<String>) -> Self {
self.alt_txt = Some(alt_txt.into());
self
}
pub fn snippet_type(mut self, snippet_type: impl Into<String>) -> Self {
self.snippet_type = Some(snippet_type.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct UploadDestination {
channel_id: Option<String>,
thread_ts: Option<String>,
initial_comment: Option<String>,
blocks: Option<Vec<Block>>,
}
impl UploadDestination {
pub fn channel(channel_id: impl Into<String>) -> Self {
Self {
channel_id: Some(channel_id.into()),
..Self::default()
}
}
pub fn thread_ts(mut self, thread_ts: impl Into<String>) -> Self {
self.thread_ts = Some(thread_ts.into());
self
}
pub fn initial_comment(mut self, initial_comment: impl Into<String>) -> Self {
self.initial_comment = Some(initial_comment.into());
self
}
pub fn blocks(mut self, blocks: Vec<Block>) -> Self {
self.blocks = Some(blocks);
self
}
}
impl SlackClient {
pub async fn upload_files(
&self,
files: Vec<FileUpload>,
destination: UploadDestination,
) -> Result<FilesCompleteUploadExternalResponse, SlackError> {
let mut tasks = JoinSet::new();
for (index, file) in files.into_iter().enumerate() {
let client = self.clone();
tasks.spawn(async move { client.upload_one(file).await.map(|entry| (index, entry)) });
}
let mut uploaded = Vec::with_capacity(tasks.len());
while let Some(joined) = tasks.join_next().await {
uploaded.push(joined.map_err(|e| SlackError::Task(e.to_string()))??);
}
uploaded.sort_by_key(|(index, _)| *index);
let mut request =
FilesCompleteUploadExternalRequest::new(uploaded.into_iter().map(|(_, e)| e).collect());
request.channel_id = destination.channel_id;
request.thread_ts = destination.thread_ts;
request.initial_comment = destination.initial_comment;
request.blocks = destination.blocks;
self.files_complete_upload_external(&request).await
}
async fn upload_one(&self, file: FileUpload) -> Result<serde_json::Value, SlackError> {
let mut request =
FilesGetUploadUrlExternalRequest::new(file.content.len() as i64, file.filename.clone());
request.alt_txt = file.alt_txt;
request.snippet_type = file.snippet_type;
let target = self.files_get_upload_url_external(&request).await?;
let (Some(upload_url), Some(file_id)) = (target.upload_url, target.file_id) else {
return Err(SlackError::MissingField(
"files.getUploadURLExternal: upload_url / file_id",
));
};
let response = self
.http()
.post(&upload_url)
.body(file.content)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(SlackError::Http {
status: status.as_u16(),
body,
});
}
Ok(json!({ "id": file_id, "title": file.title.unwrap_or(file.filename) }))
}
}