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