Skip to main content

tuwunel_api/client/
media_legacy.rs

1#![expect(deprecated)]
2
3use axum::extract::State;
4use reqwest::Url;
5use ruma::{
6	Mxc,
7	api::client::media::{
8		get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
9		get_media_preview,
10	},
11};
12use tuwunel_core::{
13	Err, Result, err,
14	utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
15};
16use tuwunel_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, Media};
17
18use crate::{ClientIp, Ruma};
19
20/// # `GET /_matrix/media/v3/config`
21///
22/// Returns max upload size.
23pub(crate) async fn get_media_config_legacy_route(
24	State(services): State<crate::State>,
25	_body: Ruma<get_media_config::v3::Request>,
26) -> Result<get_media_config::v3::Response> {
27	Ok(get_media_config::v3::Response {
28		upload_size: ruma_from_usize(services.server.config.max_request_size),
29	})
30}
31
32/// # `GET /_matrix/media/v3/preview_url`
33///
34/// Returns URL preview.
35#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy", level = "debug")]
36pub(crate) async fn get_media_preview_legacy_route(
37	State(services): State<crate::State>,
38	ClientIp(client): ClientIp,
39	body: Ruma<get_media_preview::v3::Request>,
40) -> Result<get_media_preview::v3::Response> {
41	let sender_user = body.sender_user();
42
43	let url = &body.url;
44	let url = Url::parse(&body.url).map_err(|e| {
45		err!(Request(InvalidParam(
46			debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
47		)))
48	})?;
49
50	if !services.media.url_preview_allowed(&url) {
51		return Err!(Request(Forbidden(
52			debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
53		)));
54	}
55
56	let preview = services
57		.media
58		.get_url_preview(&url)
59		.await
60		.map_err(|e| {
61			err!(Request(Unknown(
62				debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
63			)))
64		})?;
65
66	serde_json::value::to_raw_value(&preview)
67		.map(get_media_preview::v3::Response::from_raw_value)
68		.map_err(|error| {
69			err!(Request(Unknown(
70				debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
71			)))
72		})
73}
74
75/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
76///
77/// Load media from our server or over federation.
78///
79/// - Only allows federation if `allow_remote` is true
80/// - Only redirects if `allow_redirect` is true
81/// - Uses client-provided `timeout_ms` if available, else defaults to 20
82///   seconds
83#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
84pub(crate) async fn get_content_legacy_route(
85	State(services): State<crate::State>,
86	ClientIp(client): ClientIp,
87	body: Ruma<get_content::v3::Request>,
88) -> Result<get_content::v3::Response> {
89	let mxc = Mxc {
90		server_name: &body.server_name,
91		media_id: &body.media_id,
92	};
93
94	match services
95		.media
96		.get(&mxc, Some(body.timeout_ms))
97		.await
98	{
99		| Ok(Media {
100			content,
101			content_type,
102			content_disposition,
103		}) => {
104			let content_disposition = make_content_disposition(
105				content_disposition.as_ref(),
106				content_type.as_deref(),
107				None,
108			);
109
110			Ok(get_content::v3::Response {
111				file: content,
112				content_type: content_type.map(Into::into),
113				content_disposition: Some(content_disposition),
114				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
115				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
116			})
117		},
118		| Err(e) =>
119			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
120				let response = services
121					.media
122					.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
123					.await
124					.map_err(|e| {
125						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
126					})?;
127
128				let content_disposition = make_content_disposition(
129					response.content_disposition.as_ref(),
130					response.content_type.as_deref(),
131					None,
132				);
133
134				Ok(get_content::v3::Response {
135					file: response.file,
136					content_type: response.content_type,
137					content_disposition: Some(content_disposition),
138					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
139					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
140				})
141			} else {
142				Err(e)
143			},
144	}
145}
146
147/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
148///
149/// Load media from our server or over federation, permitting desired filename.
150///
151/// - Only allows federation if `allow_remote` is true
152/// - Only redirects if `allow_redirect` is true
153/// - Uses client-provided `timeout_ms` if available, else defaults to 20
154///   seconds
155#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
156pub(crate) async fn get_content_as_filename_legacy_route(
157	State(services): State<crate::State>,
158	ClientIp(client): ClientIp,
159	body: Ruma<get_content_as_filename::v3::Request>,
160) -> Result<get_content_as_filename::v3::Response> {
161	let mxc = Mxc {
162		server_name: &body.server_name,
163		media_id: &body.media_id,
164	};
165
166	match services
167		.media
168		.get(&mxc, Some(body.timeout_ms))
169		.await
170	{
171		| Ok(Media {
172			content,
173			content_type,
174			content_disposition,
175		}) => {
176			let content_disposition = make_content_disposition(
177				content_disposition.as_ref(),
178				content_type.as_deref(),
179				Some(&body.filename),
180			);
181
182			Ok(get_content_as_filename::v3::Response {
183				file: content,
184				content_type: content_type.map(Into::into),
185				content_disposition: Some(content_disposition),
186				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
187				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
188			})
189		},
190		| Err(e) =>
191			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
192				let response = services
193					.media
194					.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
195					.await
196					.map_err(|e| {
197						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
198					})?;
199
200				let content_disposition = make_content_disposition(
201					response.content_disposition.as_ref(),
202					response.content_type.as_deref(),
203					None,
204				);
205
206				Ok(get_content_as_filename::v3::Response {
207					content_disposition: Some(content_disposition),
208					content_type: response.content_type,
209					file: response.file,
210					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
211					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
212				})
213			} else {
214				Err(e)
215			},
216	}
217}
218
219/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
220///
221/// Load media thumbnail from our server or over federation.
222///
223/// - Only allows federation if `allow_remote` is true
224/// - Only redirects if `allow_redirect` is true
225/// - Uses client-provided `timeout_ms` if available, else defaults to 20
226///   seconds
227#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy", level = "debug")]
228pub(crate) async fn get_content_thumbnail_legacy_route(
229	State(services): State<crate::State>,
230	ClientIp(client): ClientIp,
231	body: Ruma<get_content_thumbnail::v3::Request>,
232) -> Result<get_content_thumbnail::v3::Response> {
233	let mxc = Mxc {
234		server_name: &body.server_name,
235		media_id: &body.media_id,
236	};
237
238	let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
239	match services
240		.media
241		.get_thumbnail(&mxc, &dim, Some(body.timeout_ms))
242		.await
243	{
244		| Ok(Media {
245			content,
246			content_type,
247			content_disposition,
248		}) => {
249			let content_disposition = make_content_disposition(
250				content_disposition.as_ref(),
251				content_type.as_deref(),
252				None,
253			);
254
255			Ok(get_content_thumbnail::v3::Response {
256				file: content,
257				content_type: content_type.map(Into::into),
258				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
259				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
260				content_disposition: Some(content_disposition),
261			})
262		},
263		| Err(e) =>
264			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
265				let response = services
266					.media
267					.fetch_remote_thumbnail_legacy(&body)
268					.await
269					.map_err(|e| {
270						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
271					})?;
272
273				let content_disposition = make_content_disposition(
274					response.content_disposition.as_ref(),
275					response.content_type.as_deref(),
276					None,
277				);
278
279				Ok(get_content_thumbnail::v3::Response {
280					file: response.file,
281					content_type: response.content_type,
282					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
283					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
284					content_disposition: Some(content_disposition),
285				})
286			} else {
287				Err(e)
288			},
289	}
290}