1use std::path::Path;
2
3use image::GenericImageView;
4
5use crate::{
6 client::SteamUser,
7 endpoint::steam_endpoint,
8 error::SteamUserError,
9 types::{
10 BeginFileUploadResult, CommitFileDetails, CommitFileUploadParams, CommitFileUploadResponse, CommitFileUploadResult,
11 file_upload::{BeginFileUploadRaw, CommitFileUploadRaw},
12 },
13};
14
15impl SteamUser {
16 #[steam_endpoint(POST, host = Community, path = "/chat/beginfileupload", kind = Upload)]
42 pub async fn begin_file_upload(&self, file_path: impl AsRef<Path>) -> Result<BeginFileUploadResult, SteamUserError> {
43 let file_path = file_path.as_ref();
44
45 let file_size = tokio::fs::metadata(file_path).await?.len();
47
48 let file_name = file_path.file_name().and_then(|n| n.to_str()).ok_or_else(|| SteamUserError::InvalidInput("Invalid file name".to_string()))?.to_string();
49
50 let img = image::open(file_path).map_err(|e| SteamUserError::InvalidImageFormat(format!("Failed to open image: {e}")))?;
51 let (width, height) = img.dimensions();
52 let format = image::ImageFormat::from_path(file_path).ok();
53
54 let type_str = match format {
55 Some(image::ImageFormat::WebP) => "png",
56 Some(image::ImageFormat::Png) => "png",
57 Some(image::ImageFormat::Jpeg) => "jpg",
58 Some(image::ImageFormat::Gif) => "gif",
59 Some(image::ImageFormat::Bmp) => "bmp",
60 Some(image::ImageFormat::Tiff) => "tiff",
61 _ => "png",
62 };
63
64 let file_type = format!("image/{}", type_str);
65
66 let sha1: String = (0..40).map(|_| format!("{:x}", rand::random::<u8>() % 16)).collect();
68
69 let raw: BeginFileUploadRaw = self
71 .post_path("/chat/beginfileupload")
72 .header("Referer", "https://steamcommunity.com/chat/")
73 .form(&[("l", "english"), ("file_size", &file_size.to_string()), ("file_name", &file_name), ("file_sha", &sha1), ("file_image_width", &width.to_string()), ("file_image_height", &height.to_string()), ("file_type", &file_type)])
74 .send()
75 .await?
76 .json()
77 .await?;
78
79 let success = i32::try_from(raw.success).unwrap_or(0);
81 if success != 1 {
82 return Err(SteamUserError::SteamError(format!("Begin upload failed with code {}", success)));
83 }
84
85 let result = raw.result.ok_or_else(|| SteamUserError::MalformedResponse("Missing result object".to_string()))?;
86 let use_https = i32::try_from(result.use_https).unwrap_or(0);
87
88 Ok(BeginFileUploadResult {
89 success,
90 url_host: result.url_host,
91 url_path: result.url_path,
92 use_https,
93 request_headers: result.request_headers,
94 timestamp: result.timestamp,
95 ugcid: result.ugcid,
96 hmac: result.hmac,
97 file_name,
98 file_sha: sha1,
99 file_image_width: width,
100 file_image_height: height,
101 file_type,
102 })
103 }
104
105 #[tracing::instrument(skip(self, file_path, begin_result), fields(url_host = %begin_result.url_host))]
129 pub async fn do_file_upload(&self, file_path: impl AsRef<Path>, begin_result: &BeginFileUploadResult) -> Result<(), SteamUserError> {
130 let file_path = file_path.as_ref();
131 let file_bytes = tokio::fs::read(file_path).await?;
132
133 let protocol = if begin_result.use_https == 1 { "https" } else { "http" };
134 let url = format!("{}://{}{}", protocol, begin_result.url_host, begin_result.url_path);
135
136 let mut req = self.request(reqwest::Method::PUT, &url);
137
138 for header in &begin_result.request_headers {
139 if header.name.eq_ignore_ascii_case("Content-Length") || header.name.eq_ignore_ascii_case("Host") {
140 continue;
141 }
142 req = req.header(&header.name, &header.value);
143 }
144
145 let response = req.body(file_bytes).send().await?;
146
147 if !response.status().is_success() {
148 return Err(SteamUserError::HttpStatus { status: response.status().as_u16(), url: response.url().to_string() });
149 }
150
151 Ok(())
152 }
153
154 #[steam_endpoint(POST, host = Community, path = "/chat/commitfileupload/", kind = Upload)]
196 pub async fn commit_file_upload(&self, params: CommitFileUploadParams) -> Result<CommitFileUploadResponse, SteamUserError> {
197 let mut form_fields = vec![
198 ("l", "english".to_string()),
199 ("file_name", params.file_name),
200 ("success", "1".to_string()),
201 ("file_sha", params.file_sha),
202 ("file_image_width", params.file_image_width.to_string()),
203 ("file_image_height", params.file_image_height.to_string()),
204 ("file_type", params.file_type),
205 ("ugcid", params.ugcid),
206 ("timestamp", params.timestamp),
207 ("hmac", params.hmac),
208 ("spoiler", "0".to_string()),
209 ];
210
211 if let Some(friend_id) = params.friend_steamid {
212 form_fields.push(("friend_steamid", friend_id));
213 }
214
215 let raw: CommitFileUploadRaw = self.post_path("/chat/commitfileupload/?l=english").form(&form_fields).send().await?.json().await?;
216
217 let success = i32::try_from(raw.success).unwrap_or(0);
219
220 let details = raw.result.and_then(|r| r.details).map(|det| CommitFileDetails {
221 url: det.url.replace("https://steamusercontent-a.akamaihd.net", "https://steamuserimages-a.akamaihd.net"),
222 });
223
224 Ok(CommitFileUploadResponse { success, result: Some(CommitFileUploadResult { details }), error: raw.error })
225 }
226}