feat: Allow retrieving redacted message content (msc2815)

Still to do:
- Handling the difference between content that we have deleted and
content we never received
- Deleting the original content on command or expiry

Another question is if we have to store the full original content?
Can we get by with just storing the 'content' field?
This commit is contained in:
Jade Ellis 2025-05-28 02:07:56 +01:00
parent 88ecf61d49
commit 46b1eeb2c8
No known key found for this signature in database
GPG key ID: 8705A2A3EBF77BD2
4 changed files with 67 additions and 2 deletions

View file

@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Event, Result, err}; use conduwuit::{Err, Event, PduEvent, Result, err};
use futures::{FutureExt, TryFutureExt, future::try_join}; use futures::{FutureExt, TryFutureExt, future::try_join};
use ruma::api::client::room::get_room_event; use ruma::api::client::{error::ErrorKind, room::get_room_event};
use crate::{Ruma, client::is_ignored_pdu}; use crate::{Ruma, client::is_ignored_pdu};
@ -14,6 +14,7 @@ pub(crate) async fn get_room_event_route(
) -> Result<get_room_event::v3::Response> { ) -> Result<get_room_event::v3::Response> {
let event_id = &body.event_id; let event_id = &body.event_id;
let room_id = &body.room_id; let room_id = &body.room_id;
let sender_user = body.sender_user();
let event = services let event = services
.rooms .rooms
@ -33,6 +34,52 @@ pub(crate) async fn get_room_event_route(
return Err!(Request(Forbidden("You don't have permission to view this event."))); return Err!(Request(Forbidden("You don't have permission to view this event.")));
} }
let include_unredacted_content = body
.include_unredacted_content // User's file has this field name
.unwrap_or(false);
if include_unredacted_content && event.is_redacted() {
let is_server_admin = services
.users
.is_admin(sender_user)
.map(|is_admin| Ok(is_admin));
let can_redact_privilege = services
.rooms
.state_accessor
.user_can_redact(event_id, sender_user, room_id, false) // federation=false for local check
;
let (is_server_admin, can_redact_privilege) =
try_join(is_server_admin, can_redact_privilege).await?;
if !is_server_admin && !can_redact_privilege {
return Err!(Request(Forbidden(
"You don't have permission to view redacted content.",
)));
}
let pdu_id = match services.rooms.timeline.get_pdu_id(event_id).await {
| Ok(id) => id,
| Err(e) => {
return Err(e);
},
};
let original_content = services
.rooms
.timeline
.get_original_pdu_content(&pdu_id)
.await?;
if let Some(original_content) = original_content {
// If the original content is available, we can return it.
// event.content = to_raw_value(&original_content)?;
event = PduEvent::from_id_val(event_id, original_content)?;
} else {
return Err(conduwuit::Error::BadRequest(
ErrorKind::UnredactedContentDeleted { content_keep_ms: None },
"The original unredacted content is not in the database.",
));
}
}
debug_assert!( debug_assert!(
event.event_id() == event_id && event.room_id() == room_id, event.event_id() == event_id && event.room_id() == room_id,
"Fetched PDU must match requested" "Fetched PDU must match requested"

View file

@ -40,6 +40,7 @@ pub(crate) async fn get_supported_versions_route(
"v1.11".to_owned(), "v1.11".to_owned(),
], ],
unstable_features: BTreeMap::from_iter([ unstable_features: BTreeMap::from_iter([
("fi.mau.msc2815".to_owned(), true),
("org.matrix.e2e_cross_signing".to_owned(), true), ("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */ ("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */ ("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */

View file

@ -190,6 +190,14 @@ impl Data {
Ok(()) Ok(())
} }
/// Returns the original content of a redacted PDU.
pub(super) async fn get_original_pdu_content(
&self,
pdu_id: &RawPduId,
) -> Result<Option<CanonicalJsonObject>> {
self.pduid_originalcontent.get(pdu_id).await.deserialized()
}
pub(super) async fn append_pdu( pub(super) async fn append_pdu(
&self, &self,
pdu_id: &RawPduId, pdu_id: &RawPduId,

View file

@ -270,6 +270,15 @@ impl Service {
self.db.store_redacted_pdu_content(pdu_id, pdu_json).await self.db.store_redacted_pdu_content(pdu_id, pdu_json).await
} }
/// Returns the original content of a redacted PDU.
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_original_pdu_content(
&self,
pdu_id: &RawPduId,
) -> Result<Option<CanonicalJsonObject>> {
self.db.get_original_pdu_content(pdu_id).await
}
/// Creates a new persisted data unit and adds it to a room. /// Creates a new persisted data unit and adds it to a room.
/// ///
/// By this point the incoming event should be fully authenticated, no auth /// By this point the incoming event should be fully authenticated, no auth