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.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 let filename = body.filename.as_deref();
59 let content_type = body.content_type.as_deref();
60 let content_disposition = make_content_disposition(None, content_type, filename);
61 let ref mxc = Mxc {
62 server_name: services.globals.server_name(),
63 media_id: &utils::random_string(MXC_LENGTH),
64 };
65
66 services
67 .media
68 .create(mxc, Some(user), Some(&content_disposition), content_type, &body.file)
69 .await?;
70
71 Ok(create_content::v3::Response {
72 content_uri: mxc.to_string().into(),
73 blurhash: None,
74 })
75}
76
77#[tracing::instrument(
81 name = "media_create_mxc",
82 level = "debug",
83 skip_all,
84 fields(%client),
85)]
86pub(crate) async fn create_mxc_uri_route(
87 State(services): State<crate::State>,
88 ClientIp(client): ClientIp,
89 body: Ruma<create_mxc_uri::v1::Request>,
90) -> Result<create_mxc_uri::v1::Response> {
91 let user = body.sender_user();
92 let mxc = Mxc {
93 server_name: services.globals.server_name(),
94 media_id: &utils::random_string(MXC_LENGTH),
95 };
96
97 let unused_expires_at = now_millis().saturating_add(
100 services
101 .server
102 .config
103 .media_create_unused_expiration_time
104 .saturating_mul(1000),
105 );
106 services
107 .media
108 .create_pending(&mxc, user, unused_expires_at)
109 .await?;
110
111 Ok(create_mxc_uri::v1::Response {
112 content_uri: mxc.to_string().into(),
113 unused_expires_at: ruma::UInt::new(unused_expires_at).map(MilliSecondsSinceUnixEpoch),
114 })
115}
116
117#[tracing::instrument(
121 name = "media_upload_async",
122 level = "debug",
123 skip_all,
124 fields(%client),
125)]
126pub(crate) async fn create_content_async_route(
127 State(services): State<crate::State>,
128 ClientIp(client): ClientIp,
129 body: Ruma<create_content_async::v3::Request>,
130) -> Result<create_content_async::v3::Response> {
131 let user = body.sender_user();
132 let mxc = Mxc {
133 server_name: &body.server_name,
134 media_id: &body.media_id,
135 };
136
137 let filename = body.filename.as_deref();
138 let content_type = body.content_type.as_deref();
139 let content_disposition = make_content_disposition(None, content_type, filename);
140
141 services
142 .media
143 .upload_pending(&mxc, user, Some(&content_disposition), content_type, &body.file)
144 .await?;
145
146 Ok(create_content_async::v3::Response {})
147}
148
149#[tracing::instrument(
153 name = "media_thumbnail_get",
154 level = "debug",
155 skip_all,
156 fields(%client),
157)]
158pub(crate) async fn get_content_thumbnail_route(
159 State(services): State<crate::State>,
160 ClientIp(client): ClientIp,
161 body: Ruma<get_content_thumbnail::v1::Request>,
162) -> Result<get_content_thumbnail::v1::Response> {
163 let user = body.sender_user();
164
165 let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
166 let animate = Animate::from(body.animated);
167 let mxc = Mxc {
168 server_name: &body.server_name,
169 media_id: &body.media_id,
170 };
171
172 let Media {
173 content,
174 content_type,
175 content_disposition,
176 } = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim, animate).await?;
177
178 Ok(get_content_thumbnail::v1::Response {
179 file: content,
180 content_type: content_type.map(Into::into),
181 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
182 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
183 content_disposition,
184 })
185}
186
187#[tracing::instrument(
191 name = "media_get",
192 level = "debug",
193 skip_all,
194 fields(%client),
195)]
196pub(crate) async fn get_content_route(
197 State(services): State<crate::State>,
198 ClientIp(client): ClientIp,
199 body: Ruma<get_content::v1::Request>,
200) -> Result<get_content::v1::Response> {
201 let mxc = Mxc {
202 server_name: &body.server_name,
203 media_id: &body.media_id,
204 };
205
206 let Media {
207 content,
208 content_type,
209 content_disposition,
210 } = fetch_file(&services, &mxc, body.timeout_ms, None).await?;
211
212 Ok(get_content::v1::Response {
213 file: content,
214 content_type: content_type.map(Into::into),
215 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
216 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
217 content_disposition,
218 })
219}
220
221#[tracing::instrument(
225 name = "media_get_af",
226 level = "debug",
227 skip_all,
228 fields(%client),
229)]
230pub(crate) async fn get_content_as_filename_route(
231 State(services): State<crate::State>,
232 ClientIp(client): ClientIp,
233 body: Ruma<get_content_as_filename::v1::Request>,
234) -> Result<get_content_as_filename::v1::Response> {
235 let mxc = Mxc {
236 server_name: &body.server_name,
237 media_id: &body.media_id,
238 };
239
240 let Media {
241 content,
242 content_type,
243 content_disposition,
244 } = fetch_file(&services, &mxc, body.timeout_ms, Some(&body.filename)).await?;
245
246 Ok(get_content_as_filename::v1::Response {
247 file: content,
248 content_type: content_type.map(Into::into),
249 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
250 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
251 content_disposition,
252 })
253}
254
255#[tracing::instrument(
259 name = "url_preview",
260 level = "debug",
261 skip_all,
262 fields(%client),
263)]
264pub(crate) async fn get_media_preview_route(
265 State(services): State<crate::State>,
266 ClientIp(client): ClientIp,
267 body: Ruma<get_media_preview::v1::Request>,
268) -> Result<get_media_preview::v1::Response> {
269 let sender_user = body.sender_user();
270
271 let url = &body.url;
272 let url = Url::parse(&body.url).map_err(|e| {
273 err!(Request(InvalidParam(
274 debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
275 )))
276 })?;
277
278 if !services.media.url_preview_allowed(&url) {
279 return Err!(Request(Forbidden(
280 debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
281 )));
282 }
283
284 let preview = services
285 .media
286 .get_url_preview(&url)
287 .await
288 .map_err(|error| {
289 err!(Request(Unknown(
290 debug_error!(%sender_user, %url, "Failed to fetch URL preview: {error}")
291 )))
292 })?;
293
294 serde_json::value::to_raw_value(&preview)
295 .map(get_media_preview::v1::Response::from_raw_value)
296 .map_err(|error| {
297 err!(Request(Unknown(
298 debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
299 )))
300 })
301}
302
303async fn fetch_thumbnail(
304 services: &Services,
305 mxc: &Mxc<'_>,
306 user: &UserId,
307 timeout_ms: Duration,
308 dim: &Dim,
309 animate: Animate,
310) -> Result<Media> {
311 let Media {
312 content,
313 content_type,
314 content_disposition,
315 } = services
316 .media
317 .get_or_fetch_thumbnail(mxc, dim, animate, timeout_ms, user)
318 .await?;
319
320 let content_disposition = Some(make_content_disposition(
321 content_disposition.as_ref(),
322 content_type.as_deref(),
323 None,
324 ));
325
326 Ok(Media {
327 content,
328 content_type,
329 content_disposition,
330 })
331}
332
333async fn fetch_file(
334 services: &Services,
335 mxc: &Mxc<'_>,
336 timeout_ms: Duration,
337 filename: Option<&str>,
338) -> Result<Media> {
339 let Media {
340 content,
341 content_type,
342 content_disposition,
343 } = services
344 .media
345 .get_or_fetch(mxc, timeout_ms)
346 .await?;
347
348 let content_disposition = Some(make_content_disposition(
349 content_disposition.as_ref(),
350 content_type.as_deref(),
351 filename,
352 ));
353
354 Ok(Media {
355 content,
356 content_type,
357 content_disposition,
358 })
359}