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::{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 let blurhash = body.generate_blurhash.then(|| {
72 services
73 .media
74 .create_blurhash(&body.file, content_type, filename)
75 .ok()
76 .flatten()
77 });
78
79 Ok(create_content::v3::Response {
80 content_uri: mxc.to_string().into(),
81 blurhash: blurhash.flatten(),
82 })
83}
84
85#[tracing::instrument(
89 name = "media_create_mxc",
90 level = "debug",
91 skip_all,
92 fields(%client),
93)]
94pub(crate) async fn create_mxc_uri_route(
95 State(services): State<crate::State>,
96 ClientIp(client): ClientIp,
97 body: Ruma<create_mxc_uri::v1::Request>,
98) -> Result<create_mxc_uri::v1::Response> {
99 let user = body.sender_user();
100 let mxc = Mxc {
101 server_name: services.globals.server_name(),
102 media_id: &utils::random_string(MXC_LENGTH),
103 };
104
105 let unused_expires_at = now_millis().saturating_add(
108 services
109 .server
110 .config
111 .media_create_unused_expiration_time
112 .saturating_mul(1000),
113 );
114 services
115 .media
116 .create_pending(&mxc, user, unused_expires_at)
117 .await?;
118
119 Ok(create_mxc_uri::v1::Response {
120 content_uri: mxc.to_string().into(),
121 unused_expires_at: ruma::UInt::new(unused_expires_at).map(MilliSecondsSinceUnixEpoch),
122 })
123}
124
125#[tracing::instrument(
129 name = "media_upload_async",
130 level = "debug",
131 skip_all,
132 fields(%client),
133)]
134pub(crate) async fn create_content_async_route(
135 State(services): State<crate::State>,
136 ClientIp(client): ClientIp,
137 body: Ruma<create_content_async::v3::Request>,
138) -> Result<create_content_async::v3::Response> {
139 let user = body.sender_user();
140 let mxc = Mxc {
141 server_name: &body.server_name,
142 media_id: &body.media_id,
143 };
144
145 let filename = body.filename.as_deref();
146 let content_type = body.content_type.as_deref();
147 let content_disposition = make_content_disposition(None, content_type, filename);
148
149 services
150 .media
151 .upload_pending(&mxc, user, Some(&content_disposition), content_type, &body.file)
152 .await?;
153
154 Ok(create_content_async::v3::Response {})
155}
156
157#[tracing::instrument(
161 name = "media_thumbnail_get",
162 level = "debug",
163 skip_all,
164 fields(%client),
165)]
166pub(crate) async fn get_content_thumbnail_route(
167 State(services): State<crate::State>,
168 ClientIp(client): ClientIp,
169 body: Ruma<get_content_thumbnail::v1::Request>,
170) -> Result<get_content_thumbnail::v1::Response> {
171 let user = body.sender_user();
172
173 let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
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).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) -> Result<Media> {
317 let Media {
318 content,
319 content_type,
320 content_disposition,
321 } = services
322 .media
323 .get_or_fetch_thumbnail(mxc, dim, timeout_ms, user)
324 .await?;
325
326 let content_disposition = Some(make_content_disposition(
327 content_disposition.as_ref(),
328 content_type.as_deref(),
329 None,
330 ));
331
332 Ok(Media {
333 content,
334 content_type,
335 content_disposition,
336 })
337}
338
339async fn fetch_file(
340 services: &Services,
341 mxc: &Mxc<'_>,
342 timeout_ms: Duration,
343 filename: Option<&str>,
344) -> Result<Media> {
345 let Media {
346 content,
347 content_type,
348 content_disposition,
349 } = services
350 .media
351 .get_or_fetch(mxc, timeout_ms)
352 .await?;
353
354 let content_disposition = Some(make_content_disposition(
355 content_disposition.as_ref(),
356 content_type.as_deref(),
357 filename,
358 ));
359
360 Ok(Media {
361 content,
362 content_type,
363 content_disposition,
364 })
365}