From 7b8af120aaf2e03725753cc43d2f90a3cbcc3b18 Mon Sep 17 00:00:00 2001 From: Ginger Date: Wed, 4 Nov 2026 15:47:45 -0500 Subject: [PATCH] feat: Add support for MSC4322 --- Cargo.lock | 22 +++++++++--------- Cargo.toml | 5 +++-- src/api/client/media.rs | 23 ++++++++++++++++++- src/api/router.rs | 1 + src/api/server/media.rs | 4 ++++ src/service/media/data.rs | 31 +++++++++++++++++++++++--- src/service/media/mod.rs | 47 +++++++++++++++++++++++++++++++++++++-- 7 files changed, 114 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a41dde29b..032ef5e17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.17.2" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "assign", "js_int", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.17.1" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "http", "js_int", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.25.2" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "as_variant", "assign", @@ -5051,7 +5051,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.20.2" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "as_variant", "base64 0.23.1", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.35.2" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "as_variant", "indexmap 2.14.2", @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.16.1" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "bytes", "http", @@ -5127,7 +5127,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.14.0" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "js_int", "thiserror 2.0.21", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.20.2" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "as_variant", "cfg-if", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "js_int", "ruma-common", @@ -5164,7 +5164,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.22.1" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "base64 0.23.1", "ed25519-dalek 3.0.0", @@ -5181,7 +5181,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.18.0" -source = "git+https://github.com/ruma/ruma.git?rev=0ed7fba34213f18a628a56f527c6cc795cd7eef5#0ed7fba34213f18a628a56f527c6cc795cd7eef5" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62#1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 520c9abcc..c18c383eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -348,8 +348,8 @@ version = "1.1.1" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] -git = "https://github.com/ruma/ruma.git" -rev = "0ed7fba34213f18a628a56f527c6cc795cd7eef5" +git = "https://github.com/gingershaped/ruwuma.git" +rev = "1e2c07dc7b65df9a35d9e3c2cedc13857dae8a62" features = [ "appservice-api-c", "client-api", @@ -381,6 +381,7 @@ features = [ "unstable-msc4143", "unstable-msc4293", "unstable-msc4363", + "unstable-msc4322", "unstable-msc4406", "unstable-msc4439", "unstable-msc4354", diff --git a/src/api/client/media.rs b/src/api/client/media.rs index 824d98541..0434e0398 100644 --- a/src/api/client/media.rs +++ b/src/api/client/media.rs @@ -18,7 +18,7 @@ use ruma::{ get_content, get_content_as_filename, get_content_thumbnail, get_media_config, get_media_preview, }, - media::create_content, + media::{self, create_content}, }, }; use service::media::mxc::Mxc; @@ -238,6 +238,27 @@ pub(crate) async fn get_media_preview_route( }) } +/// # `POST /_matrix/client/unstable/uk.timedout.msc4322/media/redact/{server_name}/{media_id}` +pub(crate) async fn unstable_redact_media_route( + State(services): State, + body: Ruma, +) -> Result { + let user = body.identity.expect_sender_user()?; + + let mxc = Mxc { + server_name: &body.server_name, + media_id: &body.media_id, + }; + + if !(services.media.user_owns(user, &mxc).await || services.admin.user_is_admin(user).await) { + return Err!(Request(Forbidden("You cannot redact this media."))); + } + + services.media.redact(&mxc).await?; + + Ok(media::redact::unstable::Response::new()) +} + async fn fetch_thumbnail_meta( services: &Services, mxc: &Mxc<'_>, diff --git a/src/api/router.rs b/src/api/router.rs index 09681c0b1..e4d4bd6c3 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -157,6 +157,7 @@ pub fn build(router: Router, state: State) -> Router { .ruma_route(&client::get_content_as_filename_route) .ruma_route(&client::get_media_preview_route) .ruma_route(&client::get_media_config_route) + .ruma_route(&client::unstable_redact_media_route) .ruma_route(&client::get_devices_route) .ruma_route(&client::get_device_route) .ruma_route(&client::update_device_route) diff --git a/src/api/server/media.rs b/src/api/server/media.rs index 98b3ae7ed..eb8aa6c02 100644 --- a/src/api/server/media.rs +++ b/src/api/server/media.rs @@ -38,6 +38,10 @@ pub(crate) async fn get_content_route( content_disposition, ); + services + .media + .track_remote_download(&body.identity, &body.media_id); + Ok(get_content::v1::Response::new( ContentMetadata::new(), FileOrLocation::File(content), diff --git a/src/service/media/data.rs b/src/service/media/data.rs index 7416464f8..0c4573fd6 100644 --- a/src/service/media/data.rs +++ b/src/service/media/data.rs @@ -1,12 +1,15 @@ -use std::{sync::Arc, time::Duration}; +use std::{collections::HashSet, sync::Arc, time::Duration}; use conduwuit::{ - Err, Result, debug, debug_info, err, + Err, Result, at, debug, debug_info, err, utils::{ReadyExt, str_from_bytes, stream::TryIgnore, string_from_bytes}, }; use database::{Database, Interfix, Map}; use futures::StreamExt; -use ruma::{OwnedMxcUri, OwnedUserId, UserId, http_headers::ContentDisposition}; +use ruma::{ + OwnedMxcUri, OwnedServerName, OwnedUserId, ServerName, UserId, + http_headers::ContentDisposition, +}; use super::{preview::UrlPreviewData, thumbnail::Dim}; use crate::media::mxc::Mxc; @@ -15,6 +18,8 @@ pub(crate) struct Data { mediaid_file: Arc, mediaid_user: Arc, url_previews: Arc, + mediaidremote_remotedownload: Arc, + mediaid_redacted: Arc, } #[derive(Debug)] @@ -30,6 +35,8 @@ impl Data { mediaid_file: db["mediaid_file"].clone(), mediaid_user: db["mediaid_user"].clone(), url_previews: db["url_previews"].clone(), + mediaidremote_remotedownload: db["mediaidremote_remotedownload"].clone(), + mediaid_redacted: db["mediaid_redacted"].clone(), } } @@ -348,4 +355,22 @@ impl Data { audio_size, }) } + + pub(super) fn track_remote_download(&self, remote: &ServerName, media_id: &str) { + self.mediaidremote_remotedownload + .put_raw((media_id, remote), []); + } + + pub(super) async fn find_remote_downloads(&self, media_id: &str) -> HashSet { + self.mediaidremote_remotedownload + .keys_prefix::<(String, OwnedServerName), _>(media_id) + .ignore_err() + .map(at!(1)) + .collect() + .await + } + + pub(super) fn mark_redacted(&self, media_id: &str) { + self.mediaid_redacted.insert(media_id, []); + } } diff --git a/src/service/media/mod.rs b/src/service/media/mod.rs index 7e229d9cf..b9458cbaa 100644 --- a/src/service/media/mod.rs +++ b/src/service/media/mod.rs @@ -5,7 +5,7 @@ mod preview; mod remote; mod tests; mod thumbnail; -use std::{path::PathBuf, sync::Arc, time::SystemTime}; +use std::{collections::HashSet, path::PathBuf, sync::Arc, time::SystemTime}; use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose}; @@ -19,7 +19,11 @@ use conduwuit::{ }, warn, }; -use ruma::{OwnedMxcUri, UserId, http_headers::ContentDisposition}; +use ruma::{ + OwnedMxcUri, OwnedServerName, ServerName, UserId, + api::federation::transactions::edu::{Edu, MediaRedactionContent}, + http_headers::ContentDisposition, +}; use tokio::{ fs, io::{AsyncReadExt, AsyncWriteExt, BufReader}, @@ -158,6 +162,31 @@ impl Service { } } + /// Mark a media file as redacted and delete it from the database. + pub async fn redact(&self, mxc: &Mxc<'_>) -> Result<()> { + self.db.mark_redacted(mxc.media_id); + self.delete(mxc).await?; + + let edu = Edu::MediaRedaction(MediaRedactionContent::new(mxc.media_id.to_owned())); + + for server in self.find_remote_downloads(mxc.media_id).await { + if let Err(err) = self.services.sending.send_edu_server(&server, &edu) { + error!(?server, %mxc, ?err, "Failed to queue media redaction EDU"); + } + } + + Ok(()) + } + + /// Check if a user uploaded a file with the given MXC. + pub async fn user_owns(&self, user: &UserId, mxc: &Mxc<'_>) -> bool { + // TODO: do this more efficiently when we refactor this garbage fire of + // a service + + let mxc = OwnedMxcUri::from(mxc.to_string()); + self.db.get_all_user_mxcs(user).await.contains(&mxc) + } + /// Deletes all media by the specified user /// /// currently, this is only practical for local users @@ -255,6 +284,20 @@ impl Service { Ok(mxcs) } + /// Mark a remote server as having downloaded a media file. + pub fn track_remote_download(&self, remote: &ServerName, media_id: &str) { + self.db.track_remote_download(remote, media_id); + } + + /// Find all remote servers that have downloaded the local media file with + /// the given ID. + pub fn find_remote_downloads( + &self, + media_id: &str, + ) -> impl Future> { + self.db.find_remote_downloads(media_id) + } + /// Deletes all media files in the given time frame. /// Returns a usize with the amount of media files deleted. pub async fn delete_all_media_within_timeframe(