From ef7eb0848c0d5dd5c8f715e40a2d64e9618742f7 Mon Sep 17 00:00:00 2001 From: Jacob Taylor Date: Sat, 3 Oct 2026 12:33:37 -0700 Subject: [PATCH] fix: Merge nex/fix-redactions-boogaloo-2 --- src/api/client/send.rs | 25 ++++++- src/core/matrix/event/redact.rs | 107 +++++++++++++++++++++++++-- src/service/rooms/timeline/create.rs | 15 ++-- 3 files changed, 132 insertions(+), 15 deletions(-) diff --git a/src/api/client/send.rs b/src/api/client/send.rs index ff07073f0..b87f7eb2e 100644 --- a/src/api/client/send.rs +++ b/src/api/client/send.rs @@ -6,7 +6,10 @@ matrix::pdu::{PartialPdu, sticky}, utils, }; -use ruma::{api::client::message::send_message_event, events::MessageLikeEventType}; +use ruma::{ + OwnedEventId, api::client::message::send_message_event, events::MessageLikeEventType, +}; +use serde::Deserialize; use serde_json::from_str; use crate::{Ruma, client_ip::ClientIp}; @@ -77,6 +80,13 @@ pub(crate) async fn send_message_event_route( let content = from_str(body.body.body.json().get()) .map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?; + let redacts = if body.body.event_type == MessageLikeEventType::RoomRedaction { + from_str::(body.body.body.json().get()) + .ok() + .and_then(|r| r.redacts) + } else { + None + }; let event_id = services .rooms @@ -92,11 +102,14 @@ pub(crate) async fn send_message_event_route( .flatten() .map(sticky::object), unsigned: Some(unsigned), - timestamp: if body.identity.is_appservice() { + timestamp: if body.identity.is_appservice() + || services.admin.user_is_admin(sender_user).await + { body.timestamp } else { None }, + redacts, ..Default::default() }, sender_user, @@ -116,3 +129,11 @@ pub(crate) async fn send_message_event_route( Ok(send_message_event::v3::Response::new(event_id)) } + +/// Struct containing exclusively the `redacts` field for MSC4169 compatible +/// redaction moving. +#[derive(Deserialize)] +struct MaybeRedaction { + #[serde(default)] + redacts: Option, +} diff --git a/src/core/matrix/event/redact.rs b/src/core/matrix/event/redact.rs index 31414d5bf..ea96fe785 100644 --- a/src/core/matrix/event/redact.rs +++ b/src/core/matrix/event/redact.rs @@ -56,7 +56,12 @@ pub(super) fn is_redacted(event: &E) -> bool { return false; }; - unsigned.redacted_because.is_some() + unsigned.redacted_by.is_some() +} + +#[derive(Deserialize)] +struct RedactionField { + redacts: OwnedEventId, } #[must_use] @@ -69,16 +74,104 @@ pub(super) fn redacts_id( } if room_version_rules.redaction.content_field_redacts { - event.redacts().map(ToOwned::to_owned) - } else { event - .get_content::() - .ok()? - .redacts + .get_content::() + .map(|r| r.redacts) + .ok() + } else { + event.redacts().map(ToOwned::to_owned) } } #[derive(Deserialize)] struct ExtractRedactedBecause { - redacted_because: Option, + #[serde(rename = "org.continuwuity.redacted_by")] + redacted_by: Option, +} + +#[cfg(test)] +mod tests { + use ruma::{UInt, events::TimelineEventType, room_version_rules::RoomVersionRules}; + use serde_json::json; + + use crate::{Event, Pdu, pdu::EventHash}; + + #[test] + fn v11_redacts_from_content() { + let p = Pdu { + event_id: ruma::owned_event_id!("$v11"), + room_id: Some(ruma::owned_room_id!("!v11:example.com")), + sender: ruma::owned_user_id!("@user:example.com"), + origin: None, + origin_server_ts: UInt::default(), + kind: TimelineEventType::RoomRedaction, + content: serde_json::value::to_raw_value(&json!({"redacts": "$content_redacts"})) + .unwrap(), + sticky: None, + state_key: None, + prev_events: vec![], + depth: UInt::default(), + auth_events: vec![], + redacts: Some(ruma::owned_event_id!("$apex_redacts")), + unsigned: None, + hashes: EventHash { sha256: String::new() }, + signatures: None, + }; + assert_eq!( + p.redacts_id(&RoomVersionRules::V11), + Some(ruma::owned_event_id!("$content_redacts")) + ); + } + + #[test] + fn v10_redacts_from_apex() { + let p = Pdu { + event_id: ruma::owned_event_id!("$v11"), + room_id: Some(ruma::owned_room_id!("!v11:example.com")), + sender: ruma::owned_user_id!("@user:example.com"), + origin: None, + origin_server_ts: UInt::default(), + kind: TimelineEventType::RoomRedaction, + content: serde_json::value::to_raw_value(&json!({"redacts": "$content_redacts"})) + .unwrap(), + sticky: None, + state_key: None, + prev_events: vec![], + depth: UInt::default(), + auth_events: vec![], + redacts: Some(ruma::owned_event_id!("$apex_redacts")), + unsigned: None, + hashes: EventHash { sha256: String::new() }, + signatures: None, + }; + assert_eq!( + p.redacts_id(&RoomVersionRules::V10), + Some(ruma::owned_event_id!("$apex_redacts")) + ); + } + + #[test] + fn redacts_ignores_non_redaction_event() { + let p = Pdu { + event_id: ruma::owned_event_id!("$v11"), + room_id: Some(ruma::owned_room_id!("!v11:example.com")), + sender: ruma::owned_user_id!("@user:example.com"), + origin: None, + origin_server_ts: UInt::default(), + kind: TimelineEventType::RoomMessage, + content: serde_json::value::to_raw_value(&json!({"redacts": "$content_redacts"})) + .unwrap(), + sticky: None, + state_key: None, + prev_events: vec![], + depth: UInt::default(), + auth_events: vec![], + redacts: Some(ruma::owned_event_id!("$apex_redacts")), + unsigned: None, + hashes: EventHash { sha256: String::new() }, + signatures: None, + }; + assert_eq!(p.redacts_id(&RoomVersionRules::V11), None); + assert_eq!(p.redacts_id(&RoomVersionRules::V10), None); + } } diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index 7d3ee9089..2f33db721 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -183,11 +183,6 @@ pub async fn create_event( .room_state_get(room_id, &event_type.clone().to_string().into(), state_key) .await { - unsigned.insert("prev_content".to_owned(), prev_pdu.get_content_as_value()); - unsigned.insert( - "prev_sender".to_owned(), - serde_json::to_value(prev_pdu.sender())?, - ); unsigned.insert( "replaces_state".to_owned(), serde_json::to_value(prev_pdu.event_id())?, @@ -195,9 +190,17 @@ pub async fn create_event( } } } + let redacts = match redacts { + // If this isn't a real redaction event, there's no redacts field. + | _ if event_type != TimelineEventType::RoomRedaction || state_key.is_some() => None, + // If the redaction is itself in the event content, we don't set the top-level field. + | Some(_) if room_version_rules.redaction.content_field_redacts => None, + // Otherwise, we do if it's present + | other => other, + }; let pdu = PduEvent { - event_id: ruma::event_id!("$thiswillbefilledinlater").into(), + event_id: ruma::owned_event_id!("$thiswillbefilledinlater"), room_id: room_id.map(ToOwned::to_owned), sender: sender.to_owned(), origin: None,