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 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 mxc = Mxc {
167 server_name: &body.server_name,
168 media_id: &body.media_id,
169 };
170
171 let Media {
172 content,
173 content_type,
174 content_disposition,
175 } = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await?;
176
177 Ok(get_content_thumbnail::v1::Response {
178 file: content,
179 content_type: content_type.map(Into::into),
180 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
181 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
182 content_disposition,
183 })
184}
185
186#[tracing::instrument(
190 name = "media_get",
191 level = "debug",
192 skip_all,
193 fields(%client),
194)]
195pub(crate) async fn get_content_route(
196 State(services): State<crate::State>,
197 ClientIp(client): ClientIp,
198 body: Ruma<get_content::v1::Request>,
199) -> Result<get_content::v1::Response> {
200 let mxc = Mxc {
201 server_name: &body.server_name,
202 media_id: &body.media_id,
203 };
204
205 let Media {
206 content,
207 content_type,
208 content_disposition,
209 } = fetch_file(&services, &mxc, body.timeout_ms, None).await?;
210
211 Ok(get_content::v1::Response {
212 file: content,
213 content_type: content_type.map(Into::into),
214 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
215 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
216 content_disposition,
217 })
218}
219
220#[tracing::instrument(
224 name = "media_get_af",
225 level = "debug",
226 skip_all,
227 fields(%client),
228)]
229pub(crate) async fn get_content_as_filename_route(
230 State(services): State<crate::State>,
231 ClientIp(client): ClientIp,
232 body: Ruma<get_content_as_filename::v1::Request>,
233) -> Result<get_content_as_filename::v1::Response> {
234 let mxc = Mxc {
235 server_name: &body.server_name,
236 media_id: &body.media_id,
237 };
238
239 let Media {
240 content,
241 content_type,
242 content_disposition,
243 } = fetch_file(&services, &mxc, body.timeout_ms, Some(&body.filename)).await?;
244
245 Ok(get_content_as_filename::v1::Response {
246 file: content,
247 content_type: content_type.map(Into::into),
248 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
249 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
250 content_disposition,
251 })
252}
253
254#[tracing::instrument(
258 name = "url_preview",
259 level = "debug",
260 skip_all,
261 fields(%client),
262)]
263pub(crate) async fn get_media_preview_route(
264 State(services): State<crate::State>,
265 ClientIp(client): ClientIp,
266 body: Ruma<get_media_preview::v1::Request>,
267) -> Result<get_media_preview::v1::Response> {
268 let sender_user = body.sender_user();
269
270 let url = &body.url;
271 let url = Url::parse(&body.url).map_err(|e| {
272 err!(Request(InvalidParam(
273 debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
274 )))
275 })?;
276
277 if !services.media.url_preview_allowed(&url) {
278 return Err!(Request(Forbidden(
279 debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
280 )));
281 }
282
283 let preview = services
284 .media
285 .get_url_preview(&url)
286 .await
287 .map_err(|error| {
288 err!(Request(Unknown(
289 debug_error!(%sender_user, %url, "Failed to fetch URL preview: {error}")
290 )))
291 })?;
292
293 serde_json::value::to_raw_value(&preview)
294 .map(get_media_preview::v1::Response::from_raw_value)
295 .map_err(|error| {
296 err!(Request(Unknown(
297 debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
298 )))
299 })
300}
301
302async fn fetch_thumbnail(
303 services: &Services,
304 mxc: &Mxc<'_>,
305 user: &UserId,
306 timeout_ms: Duration,
307 dim: &Dim,
308) -> Result<Media> {
309 let Media {
310 content,
311 content_type,
312 content_disposition,
313 } = services
314 .media
315 .get_or_fetch_thumbnail(mxc, dim, timeout_ms, user)
316 .await?;
317
318 let content_disposition = Some(make_content_disposition(
319 content_disposition.as_ref(),
320 content_type.as_deref(),
321 None,
322 ));
323
324 Ok(Media {
325 content,
326 content_type,
327 content_disposition,
328 })
329}
330
331async fn fetch_file(
332 services: &Services,
333 mxc: &Mxc<'_>,
334 timeout_ms: Duration,
335 filename: Option<&str>,
336) -> Result<Media> {
337 let Media {
338 content,
339 content_type,
340 content_disposition,
341 } = services
342 .media
343 .get_or_fetch(mxc, timeout_ms)
344 .await?;
345
346 let content_disposition = Some(make_content_disposition(
347 content_disposition.as_ref(),
348 content_type.as_deref(),
349 filename,
350 ));
351
352 Ok(Media {
353 content,
354 content_type,
355 content_disposition,
356 })
357}