1#![expect(deprecated)]
2
3use axum::{
4 extract::State,
5 response::{IntoResponse, Redirect, Response},
6};
7use reqwest::Url;
8use ruma::{
9 Mxc,
10 api::client::media::{
11 get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
12 get_media_preview,
13 },
14};
15use tuwunel_core::{
16 Err, Result, err,
17 utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
18};
19use tuwunel_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, Media};
20
21use crate::{ClientIp, Ruma, RumaResponse};
22
23pub(crate) async fn get_media_config_legacy_route(
27 State(services): State<crate::State>,
28 _body: Ruma<get_media_config::v3::Request>,
29) -> Result<get_media_config::v3::Response> {
30 Ok(get_media_config::v3::Response {
31 upload_size: ruma_from_usize(services.server.config.max_request_size),
32 })
33}
34
35#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy", level = "debug")]
39pub(crate) async fn get_media_preview_legacy_route(
40 State(services): State<crate::State>,
41 ClientIp(client): ClientIp,
42 body: Ruma<get_media_preview::v3::Request>,
43) -> Result<get_media_preview::v3::Response> {
44 let sender_user = body.sender_user();
45
46 let url = &body.url;
47 let url = Url::parse(&body.url).map_err(|e| {
48 err!(Request(InvalidParam(
49 debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
50 )))
51 })?;
52
53 if !services.media.url_preview_allowed(&url) {
54 return Err!(Request(Forbidden(
55 debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
56 )));
57 }
58
59 let preview = services
60 .media
61 .get_url_preview(&url)
62 .await
63 .map_err(|e| {
64 err!(Request(Unknown(
65 debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
66 )))
67 })?;
68
69 serde_json::value::to_raw_value(&preview)
70 .map(get_media_preview::v3::Response::from_raw_value)
71 .map_err(|error| {
72 err!(Request(Unknown(
73 debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
74 )))
75 })
76}
77
78#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
87pub(crate) async fn get_content_legacy_route(
88 State(services): State<crate::State>,
89 ClientIp(client): ClientIp,
90 body: Ruma<get_content::v3::Request>,
91) -> Result<Response> {
92 let mxc = Mxc {
93 server_name: &body.server_name,
94 media_id: &body.media_id,
95 };
96
97 if body.allow_redirect
98 && services.globals.server_is_ours(&body.server_name)
99 && let Some(url) = services
100 .media
101 .redirect_url(&mxc, &Dim::default())
102 .await?
103 {
104 return Ok(Redirect::temporary(url.as_str()).into_response());
105 }
106
107 match services
108 .media
109 .get(&mxc, Some(body.timeout_ms))
110 .await
111 {
112 | Ok(Media {
113 content,
114 content_type,
115 content_disposition,
116 }) => {
117 let content_disposition = make_content_disposition(
118 content_disposition.as_ref(),
119 content_type.as_deref(),
120 None,
121 );
122
123 let response = get_content::v3::Response {
124 file: content,
125 content_type: content_type.map(Into::into),
126 content_disposition: Some(content_disposition),
127 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
128 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
129 };
130
131 Ok(RumaResponse(response).into_response())
132 },
133 | Err(e) =>
134 if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
135 let response = services
136 .media
137 .fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
138 .await
139 .map_err(|e| {
140 err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
141 })?;
142
143 let content_disposition = make_content_disposition(
144 response.content_disposition.as_ref(),
145 response.content_type.as_deref(),
146 None,
147 );
148
149 let response = get_content::v3::Response {
150 file: response.file,
151 content_type: response.content_type,
152 content_disposition: Some(content_disposition),
153 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
154 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
155 };
156
157 Ok(RumaResponse(response).into_response())
158 } else {
159 Err(e)
160 },
161 }
162}
163
164#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
173pub(crate) async fn get_content_as_filename_legacy_route(
174 State(services): State<crate::State>,
175 ClientIp(client): ClientIp,
176 body: Ruma<get_content_as_filename::v3::Request>,
177) -> Result<Response> {
178 let mxc = Mxc {
179 server_name: &body.server_name,
180 media_id: &body.media_id,
181 };
182
183 if body.allow_redirect
184 && services.globals.server_is_ours(&body.server_name)
185 && let Some(url) = services
186 .media
187 .redirect_url(&mxc, &Dim::default())
188 .await?
189 {
190 return Ok(Redirect::temporary(url.as_str()).into_response());
191 }
192
193 match services
194 .media
195 .get(&mxc, Some(body.timeout_ms))
196 .await
197 {
198 | Ok(Media {
199 content,
200 content_type,
201 content_disposition,
202 }) => {
203 let content_disposition = make_content_disposition(
204 content_disposition.as_ref(),
205 content_type.as_deref(),
206 Some(&body.filename),
207 );
208
209 let response = get_content_as_filename::v3::Response {
210 file: content,
211 content_type: content_type.map(Into::into),
212 content_disposition: Some(content_disposition),
213 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
214 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
215 };
216
217 Ok(RumaResponse(response).into_response())
218 },
219 | Err(e) =>
220 if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
221 let response = services
222 .media
223 .fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
224 .await
225 .map_err(|e| {
226 err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
227 })?;
228
229 let content_disposition = make_content_disposition(
230 response.content_disposition.as_ref(),
231 response.content_type.as_deref(),
232 None,
233 );
234
235 let response = get_content_as_filename::v3::Response {
236 content_disposition: Some(content_disposition),
237 content_type: response.content_type,
238 file: response.file,
239 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
240 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
241 };
242
243 Ok(RumaResponse(response).into_response())
244 } else {
245 Err(e)
246 },
247 }
248}
249
250#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy", level = "debug")]
259pub(crate) async fn get_content_thumbnail_legacy_route(
260 State(services): State<crate::State>,
261 ClientIp(client): ClientIp,
262 body: Ruma<get_content_thumbnail::v3::Request>,
263) -> Result<Response> {
264 let mxc = Mxc {
265 server_name: &body.server_name,
266 media_id: &body.media_id,
267 };
268
269 let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
270
271 if body.allow_redirect
272 && services.globals.server_is_ours(&body.server_name)
273 && let Some(url) = services.media.redirect_url(&mxc, &dim).await?
274 {
275 return Ok(Redirect::temporary(url.as_str()).into_response());
276 }
277
278 match services
279 .media
280 .get_thumbnail(&mxc, &dim, Some(body.timeout_ms))
281 .await
282 {
283 | Ok(Media {
284 content,
285 content_type,
286 content_disposition,
287 }) => {
288 let content_disposition = make_content_disposition(
289 content_disposition.as_ref(),
290 content_type.as_deref(),
291 None,
292 );
293
294 let response = get_content_thumbnail::v3::Response {
295 file: content,
296 content_type: content_type.map(Into::into),
297 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
298 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
299 content_disposition: Some(content_disposition),
300 };
301
302 Ok(RumaResponse(response).into_response())
303 },
304 | Err(e) =>
305 if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
306 let response = services
307 .media
308 .fetch_remote_thumbnail_legacy(&body)
309 .await
310 .map_err(|e| {
311 err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
312 })?;
313
314 let content_disposition = make_content_disposition(
315 response.content_disposition.as_ref(),
316 response.content_type.as_deref(),
317 None,
318 );
319
320 let response = get_content_thumbnail::v3::Response {
321 file: response.file,
322 content_type: response.content_type,
323 cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
324 cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
325 content_disposition: Some(content_disposition),
326 };
327
328 Ok(RumaResponse(response).into_response())
329 } else {
330 Err(e)
331 },
332 }
333}