1use std::time::Duration;
2
3use axum::extract::State;
4use reqwest::Url;
5use ruma::{
6 MilliSecondsSinceUnixEpoch, Mxc, UserId,
7 api::client::{
8 authenticated_media::{
9 get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
10 get_media_preview,
11 },
12 media::{create_content, create_content_async, create_mxc_uri},
13 },
14};
15use tuwunel_core::{
16 Err, Result, err,
17 utils::{
18 self, content_disposition::make_content_disposition, math::ruma_from_usize,
19 time::now_millis,
20 },
21};
22use tuwunel_service::{
23 Services,
24 media::{Animate, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, MXC_LENGTH, Media},
25};
26
27use crate::{ClientIp, Ruma};
28
29pub(crate) async fn get_media_config_route(
31 State(services): State<crate::State>,
32 _body: Ruma<get_media_config::v1::Request>,
33) -> Result<get_media_config::v1::Response> {
34 Ok(get_media_config::v1::Response {
35 upload_size: ruma_from_usize(services.server.config.http.max_request_size),
36 })
37}
38
39#[tracing::instrument(
46 name = "media_upload",
47 level = "debug",
48 skip_all,
49 fields(%client),
50)]
51pub(crate) async fn create_content_route(
52 State(services): State<crate::State>,
53 ClientIp(client): ClientIp,
54 body: Ruma<create_content::v3::Request>,
55) -> Result<create_content::v3::Response> {
56 let user = body.sender_user();
57
58 services.media.check_upload_space().await?;
59
60 let filename = body.filename.as_deref();
61 let content_type = body.content_type.as_deref();
62 let content_disposition = make_content_disposition(None, content_type, filename);
63 let ref mxc = Mxc {
64 server_name: services.globals.server_name(),
65 media_id: &utils::random_string(MXC_LENGTH),
66 };
67
68 services
69 .media
70 .create(mxc, Some(user), Some(&content_disposition), content_type, &body.file)
71 .await?;
72
73 Ok(create_content::v3::Response {
74 content_uri: mxc.to_string().into(),
75 blurhash: None,
76 })
77}
78
79#[tracing::instrument(
83 name = "media_create_mxc",
84 level = "debug",
85 skip_all,
86 fields(%client),
87)]
88pub(crate) async fn create_mxc_uri_route(
89 State(services): State<crate::State>,
90 ClientIp(client): ClientIp,
91 body: Ruma<create_mxc_uri::v1::Request>,
92) -> Result<create_mxc_uri::v1::Response> {
93 let user = body.sender_user();
94 let mxc = Mxc {
95 server_name: services.globals.server_name(),
96 media_id: &utils::random_string(MXC_LENGTH),
97 };
98
99 let unused_expires_at = now_millis().saturating_add(
102 services
103 .server
104 .config
105 .media
106 .create_unused_expiration_time
107 .saturating_mul(1000),
108 );
109
110 services
111 .media
112 .create_pending(&mxc, user, unused_expires_at)
113 .await?;
114
115 Ok(create_mxc_uri::v1::Response {
116 content_uri: mxc.to_string().into(),
117 unused_expires_at: ruma::UInt::new(unused_expires_at).map(MilliSecondsSinceUnixEpoch),
118 })
119}
120
121#[tracing::instrument(
125 name = "media_upload_async",
126 level = "debug",
127 skip_all,
128 fields(%client),
129)]
130pub(crate) async fn create_content_async_route(
131 State(services): State<crate::State>,
132 ClientIp(client): ClientIp,
133 body: Ruma<create_content_async::v3::Request>,
134) -> Result<create_content_async::v3::Response> {
135 let user = body.sender_user();
136
137 services.media.check_upload_space().await?;
138
139 let mxc = Mxc {
140 server_name: &body.server_name,
141 media_id: &body.media_id,
142 };
143
144 let filename = body.filename.as_deref();
145 let content_type = body.content_type.as_deref();
146 let content_disposition = make_content_disposition(None, content_type, filename);
147
148 services
149 .media
150 .upload_pending(&mxc, user, Some(&content_disposition), content_type, &body.file)
151 .await?;
152
153 Ok(create_content_async::v3::Response {})
154}
155
156#[tracing::instrument(
160 name = "media_thumbnail_get",
161 level = "debug",
162 skip_all,
163 fields(%client),
164)]
165pub(crate) async fn get_content_thumbnail_route(
166 State(services): State<crate::State>,
167 ClientIp(client): ClientIp,
168 body: Ruma<get_content_thumbnail::v1::Request>,
169) -> Result<get_content_thumbnail::v1::Response> {
170 let user = body.sender_user();
171
172 let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
173 let animate = Animate::from(body.animated);
174 let mxc = Mxc {
175 server_name: &body.server_name,
176 media_id: &body.media_id,
177 };
178
179 let Media {
180 content,
181 content_type,
182 content_disposition,
183 } = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim, animate).await?;
184
185 Ok(get_content_thumbnail::v1::Response {
186 file: content,
187 content_type: content_type.map(Into::into),
188 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
189 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
190 content_disposition,
191 })
192}
193
194#[tracing::instrument(
198 name = "media_get",
199 level = "debug",
200 skip_all,
201 fields(%client),
202)]
203pub(crate) async fn get_content_route(
204 State(services): State<crate::State>,
205 ClientIp(client): ClientIp,
206 body: Ruma<get_content::v1::Request>,
207) -> Result<get_content::v1::Response> {
208 let mxc = Mxc {
209 server_name: &body.server_name,
210 media_id: &body.media_id,
211 };
212
213 let Media {
214 content,
215 content_type,
216 content_disposition,
217 } = fetch_file(&services, &mxc, body.timeout_ms, None).await?;
218
219 Ok(get_content::v1::Response {
220 file: content,
221 content_type: content_type.map(Into::into),
222 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
223 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
224 content_disposition,
225 })
226}
227
228#[tracing::instrument(
232 name = "media_get_af",
233 level = "debug",
234 skip_all,
235 fields(%client),
236)]
237pub(crate) async fn get_content_as_filename_route(
238 State(services): State<crate::State>,
239 ClientIp(client): ClientIp,
240 body: Ruma<get_content_as_filename::v1::Request>,
241) -> Result<get_content_as_filename::v1::Response> {
242 let mxc = Mxc {
243 server_name: &body.server_name,
244 media_id: &body.media_id,
245 };
246
247 let Media {
248 content,
249 content_type,
250 content_disposition,
251 } = fetch_file(&services, &mxc, body.timeout_ms, Some(&body.filename)).await?;
252
253 Ok(get_content_as_filename::v1::Response {
254 file: content,
255 content_type: content_type.map(Into::into),
256 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
257 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
258 content_disposition,
259 })
260}
261
262#[tracing::instrument(
266 name = "url_preview",
267 level = "debug",
268 skip_all,
269 fields(%client),
270)]
271pub(crate) async fn get_media_preview_route(
272 State(services): State<crate::State>,
273 ClientIp(client): ClientIp,
274 body: Ruma<get_media_preview::v1::Request>,
275) -> Result<get_media_preview::v1::Response> {
276 let sender_user = body.sender_user();
277
278 let url = &body.url;
279 let url = Url::parse(&body.url).map_err(|e| {
280 err!(Request(InvalidParam(
281 debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
282 )))
283 })?;
284
285 if !services.media.url_preview_allowed(&url) {
286 return Err!(Request(Forbidden(
287 debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
288 )));
289 }
290
291 let preview = services
292 .media
293 .get_url_preview(&url)
294 .await
295 .map_err(|error| {
296 err!(Request(Unknown(
297 debug_error!(%sender_user, %url, "Failed to fetch URL preview: {error}")
298 )))
299 })?;
300
301 serde_json::value::to_raw_value(&preview)
302 .map(get_media_preview::v1::Response::from_raw_value)
303 .map_err(|error| {
304 err!(Request(Unknown(
305 debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
306 )))
307 })
308}
309
310async fn fetch_thumbnail(
311 services: &Services,
312 mxc: &Mxc<'_>,
313 user: &UserId,
314 timeout_ms: Duration,
315 dim: &Dim,
316 animate: Animate,
317) -> Result<Media> {
318 let Media {
319 content,
320 content_type,
321 content_disposition,
322 } = services
323 .media
324 .get_or_fetch_thumbnail(mxc, dim, animate, timeout_ms, user)
325 .await?;
326
327 let content_disposition = Some(make_content_disposition(
328 content_disposition.as_ref(),
329 content_type.as_deref(),
330 None,
331 ));
332
333 Ok(Media {
334 content,
335 content_type,
336 content_disposition,
337 })
338}
339
340async fn fetch_file(
341 services: &Services,
342 mxc: &Mxc<'_>,
343 timeout_ms: Duration,
344 filename: Option<&str>,
345) -> Result<Media> {
346 let Media {
347 content,
348 content_type,
349 content_disposition,
350 } = services
351 .media
352 .get_or_fetch(mxc, timeout_ms)
353 .await?;
354
355 let content_disposition = Some(make_content_disposition(
356 content_disposition.as_ref(),
357 content_type.as_deref(),
358 filename,
359 ));
360
361 Ok(Media {
362 content,
363 content_type,
364 content_disposition,
365 })
366}