Skip to content
Snippets Groups Projects
media.rs 7.77 KiB
Newer Older
  • Learn to ignore specific revisions
  • use std::time::Duration;
    
    
    use axum::extract::State;
    
    use axum_client_ip::InsecureClientIp;
    
    use conduit::{
    
    	err,
    	utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
    	Err, Result,
    };
    use conduit_service::{
    	media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, MXC_LENGTH},
    	Services,
    };
    use ruma::{
    	api::client::{
    		authenticated_media::{
    			get_content, get_content_as_filename, get_content_thumbnail, get_media_config, get_media_preview,
    		},
    		media::create_content,
    	},
    	Mxc, UserId,
    
    use crate::Ruma;
    
    /// # `GET /_matrix/client/v1/media/config`
    pub(crate) async fn get_media_config_route(
    	State(services): State<crate::State>, _body: Ruma<get_media_config::v1::Request>,
    ) -> Result<get_media_config::v1::Response> {
    	Ok(get_media_config::v1::Response {
    		upload_size: ruma_from_usize(services.globals.config.max_request_size),
    	})
    }
    
    
    /// # `POST /_matrix/media/v3/upload`
    
    ///
    /// Permanently save media in the server.
    ///
    /// - Some metadata will be saved in the database
    /// - Media will be saved in the media/ directory
    
    #[tracing::instrument(skip_all, fields(%client), name = "media_upload")]
    
    pub(crate) async fn create_content_route(
    
    	State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
    	body: Ruma<create_content::v3::Request>,
    
    ) -> Result<create_content::v3::Response> {
    
    	let user = body.sender_user.as_ref().expect("user is authenticated");
    
    	let filename = body.filename.as_deref();
    	let content_type = body.content_type.as_deref();
    	let content_disposition = make_content_disposition(None, content_type, filename);
    
    	let mxc = Mxc {
    		server_name: services.globals.server_name(),
    		media_id: &utils::random_string(MXC_LENGTH),
    	};
    
    	services
    
    		.create(&mxc, Some(user), Some(&content_disposition), content_type, &body.file)
    		.await
    		.map(|()| create_content::v3::Response {
    			content_uri: mxc.to_string().into(),
    			blurhash: None,
    		})
    }
    
    /// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
    ///
    /// Load media thumbnail from our server or over federation.
    #[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get")]
    pub(crate) async fn get_content_thumbnail_route(
    	State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
    	body: Ruma<get_content_thumbnail::v1::Request>,
    ) -> Result<get_content_thumbnail::v1::Response> {
    	let user = body.sender_user.as_ref().expect("user is authenticated");
    
    	let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
    	let mxc = Mxc {
    		server_name: &body.server_name,
    		media_id: &body.media_id,
    	};
    
    	let FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	} = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await?;
    
    	Ok(get_content_thumbnail::v1::Response {
    		file: content.expect("entire file contents"),
    		content_type: content_type.map(Into::into),
    		cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
    		cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
    		content_disposition,
    
    
    /// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
    ///
    /// Load media from our server or over federation.
    #[tracing::instrument(skip_all, fields(%client), name = "media_get")]
    pub(crate) async fn get_content_route(
    	State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
    	body: Ruma<get_content::v1::Request>,
    ) -> Result<get_content::v1::Response> {
    	let user = body.sender_user.as_ref().expect("user is authenticated");
    
    	let mxc = Mxc {
    		server_name: &body.server_name,
    		media_id: &body.media_id,
    	};
    
    	let FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	} = fetch_file(&services, &mxc, user, body.timeout_ms, None).await?;
    
    	Ok(get_content::v1::Response {
    		file: content.expect("entire file contents"),
    		content_type: content_type.map(Into::into),
    		cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
    		cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
    		content_disposition,
    	})
    }
    
    /// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
    ///
    /// Load media from our server or over federation as fileName.
    #[tracing::instrument(skip_all, fields(%client), name = "media_get_af")]
    pub(crate) async fn get_content_as_filename_route(
    	State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
    	body: Ruma<get_content_as_filename::v1::Request>,
    ) -> Result<get_content_as_filename::v1::Response> {
    	let user = body.sender_user.as_ref().expect("user is authenticated");
    
    	let mxc = Mxc {
    		server_name: &body.server_name,
    		media_id: &body.media_id,
    	};
    
    	let FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	} = fetch_file(&services, &mxc, user, body.timeout_ms, Some(&body.filename)).await?;
    
    	Ok(get_content_as_filename::v1::Response {
    		file: content.expect("entire file contents"),
    		content_type: content_type.map(Into::into),
    		cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
    		cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
    		content_disposition,
    	})
    }
    
    /// # `GET /_matrix/client/v1/media/preview_url`
    ///
    /// Returns URL preview.
    #[tracing::instrument(skip_all, fields(%client), name = "url_preview")]
    pub(crate) async fn get_media_preview_route(
    	State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
    	body: Ruma<get_media_preview::v1::Request>,
    ) -> Result<get_media_preview::v1::Response> {
    	let sender_user = body.sender_user.as_ref().expect("user is authenticated");
    
    	let url = &body.url;
    	if !services.media.url_preview_allowed(url) {
    		return Err!(Request(Forbidden(
    			debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
    		)));
    	}
    
    	let preview = services.media.get_url_preview(url).await.map_err(|error| {
    		err!(Request(Unknown(
    			debug_error!(%sender_user, %url, ?error, "Failed to fetch URL preview.")
    		)))
    	})?;
    
    	serde_json::value::to_raw_value(&preview)
    		.map(get_media_preview::v1::Response::from_raw_value)
    		.map_err(|error| {
    			err!(Request(Unknown(
    				debug_error!(%sender_user, %url, ?error, "Failed to parse URL preview.")
    			)))
    		})
    }
    
    async fn fetch_thumbnail(
    	services: &Services, mxc: &Mxc<'_>, user: &UserId, timeout_ms: Duration, dim: &Dim,
    ) -> Result<FileMeta> {
    	let FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	} = fetch_thumbnail_meta(services, mxc, user, timeout_ms, dim).await?;
    
    	let content_disposition = Some(make_content_disposition(
    		content_disposition.as_ref(),
    		content_type.as_deref(),
    		None,
    	));
    
    	Ok(FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	})
    }
    
    async fn fetch_file(
    	services: &Services, mxc: &Mxc<'_>, user: &UserId, timeout_ms: Duration, filename: Option<&str>,
    ) -> Result<FileMeta> {
    	let FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	} = fetch_file_meta(services, mxc, user, timeout_ms).await?;
    
    	let content_disposition = Some(make_content_disposition(
    		content_disposition.as_ref(),
    		content_type.as_deref(),
    		filename,
    	));
    
    	Ok(FileMeta {
    		content,
    		content_type,
    		content_disposition,
    	})
    }
    
    async fn fetch_thumbnail_meta(
    	services: &Services, mxc: &Mxc<'_>, user: &UserId, timeout_ms: Duration, dim: &Dim,
    ) -> Result<FileMeta> {
    	if let Some(filemeta) = services.media.get_thumbnail(mxc, dim).await? {
    		return Ok(filemeta);
    	}
    
    	if services.globals.server_is_ours(mxc.server_name) {
    		return Err!(Request(NotFound("Local thumbnail not found.")));
    	}
    
    	services
    		.media
    		.fetch_remote_thumbnail(mxc, Some(user), None, timeout_ms, dim)
    		.await
    }
    
    async fn fetch_file_meta(services: &Services, mxc: &Mxc<'_>, user: &UserId, timeout_ms: Duration) -> Result<FileMeta> {
    	if let Some(filemeta) = services.media.get(mxc).await? {
    		return Ok(filemeta);
    	}
    
    	if services.globals.server_is_ours(mxc.server_name) {
    		return Err!(Request(NotFound("Local media not found.")));
    	}
    
    	services
    		.media
    		.fetch_remote_content(mxc, Some(user), None, timeout_ms)
    		.await
    }