events: Add support for redacts key into content of RoomRedactionEvent

According to MSC2174
This commit is contained in:
Kévin Commaille 2023-08-10 15:22:06 +02:00 committed by Kévin Commaille
parent cf70f74fb7
commit 7a5d9b6e8b
5 changed files with 358 additions and 49 deletions

View File

@ -24,6 +24,11 @@ Breaking changes:
(MSC2746 / Matrix 1.7) (MSC2746 / Matrix 1.7)
- The `Replacement` relation for `RoomMessageEventContent` now takes a - The `Replacement` relation for `RoomMessageEventContent` now takes a
`RoomMessageEventContentWithoutRelation` instead of a `MessageType` `RoomMessageEventContentWithoutRelation` instead of a `MessageType`
- Make the `redacts` field of `Original(Sync)RoomRedactionEvent` optional to handle the format
where the `redacts` key is moved inside the `content`, as introduced in room version 11,
according to MSC2174 / MSC3820
- `RoomRedactionEventContent::new()` was renamed to `new_v1()`, and `with_reason()` is no
longer a constructor but a builder-type method
Improvements: Improvements:

View File

@ -4,19 +4,21 @@
use js_int::Int; use js_int::Int;
use ruma_macros::{Event, EventContent}; use ruma_macros::{Event, EventContent};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue; use tracing::error;
use crate::{ use crate::{
events::{ events::{
BundledMessageLikeRelations, EventContent, MessageLikeEventType, RedactedUnsigned, BundledMessageLikeRelations, EventContent, MessageLikeEventType, RedactContent,
RedactionDeHelper, RedactedMessageLikeEventContent, RedactedUnsigned, StaticEventContent,
}, },
serde::{from_raw_json_value, CanBeEmpty}, serde::CanBeEmpty,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedTransactionId, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedTransactionId,
OwnedUserId, RoomId, UserId, OwnedUserId, RoomId, RoomVersionId, UserId,
}; };
mod event_serde;
/// A possibly-redacted redaction event. /// A possibly-redacted redaction event.
#[allow(clippy::exhaustive_enums)] #[allow(clippy::exhaustive_enums)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -40,14 +42,16 @@ pub enum SyncRoomRedactionEvent {
} }
/// Redaction event. /// Redaction event.
#[derive(Clone, Debug, Event)] #[derive(Clone, Debug)]
#[allow(clippy::exhaustive_structs)] #[allow(clippy::exhaustive_structs)]
pub struct OriginalRoomRedactionEvent { pub struct OriginalRoomRedactionEvent {
/// Data specific to the event type. /// Data specific to the event type.
pub content: RoomRedactionEventContent, pub content: RoomRedactionEventContent,
/// The ID of the event that was redacted. /// The ID of the event that was redacted.
pub redacts: OwnedEventId, ///
/// This field is required in room versions prior to 11.
pub redacts: Option<OwnedEventId>,
/// The globally unique event identifier for the user who sent the event. /// The globally unique event identifier for the user who sent the event.
pub event_id: OwnedEventId, pub event_id: OwnedEventId,
@ -65,6 +69,22 @@ pub struct OriginalRoomRedactionEvent {
pub unsigned: RoomRedactionUnsigned, pub unsigned: RoomRedactionUnsigned,
} }
impl From<OriginalRoomRedactionEvent> for OriginalSyncRoomRedactionEvent {
fn from(value: OriginalRoomRedactionEvent) -> Self {
let OriginalRoomRedactionEvent {
content,
redacts,
event_id,
sender,
origin_server_ts,
unsigned,
..
} = value;
Self { content, redacts, event_id, sender, origin_server_ts, unsigned }
}
}
/// Redacted redaction event. /// Redacted redaction event.
#[derive(Clone, Debug, Event)] #[derive(Clone, Debug, Event)]
#[allow(clippy::exhaustive_structs)] #[allow(clippy::exhaustive_structs)]
@ -89,14 +109,16 @@ pub struct RedactedRoomRedactionEvent {
} }
/// Redaction event without a `room_id`. /// Redaction event without a `room_id`.
#[derive(Clone, Debug, Event)] #[derive(Clone, Debug)]
#[allow(clippy::exhaustive_structs)] #[allow(clippy::exhaustive_structs)]
pub struct OriginalSyncRoomRedactionEvent { pub struct OriginalSyncRoomRedactionEvent {
/// Data specific to the event type. /// Data specific to the event type.
pub content: RoomRedactionEventContent, pub content: RoomRedactionEventContent,
/// The ID of the event that was redacted. /// The ID of the event that was redacted.
pub redacts: OwnedEventId, ///
/// This field is required in room versions prior to 11.
pub redacts: Option<OwnedEventId>,
/// The globally unique event identifier for the user who sent the event. /// The globally unique event identifier for the user who sent the event.
pub event_id: OwnedEventId, pub event_id: OwnedEventId,
@ -112,6 +134,21 @@ pub struct OriginalSyncRoomRedactionEvent {
} }
impl OriginalSyncRoomRedactionEvent { impl OriginalSyncRoomRedactionEvent {
/// Convert this sync event into a full event, one with a `room_id` field.
pub fn into_full_event(self, room_id: OwnedRoomId) -> OriginalRoomRedactionEvent {
let Self { content, redacts, event_id, sender, origin_server_ts, unsigned } = self;
OriginalRoomRedactionEvent {
content,
redacts,
event_id,
sender,
origin_server_ts,
room_id,
unsigned,
}
}
pub(crate) fn into_maybe_redacted(self) -> SyncRoomRedactionEvent { pub(crate) fn into_maybe_redacted(self) -> SyncRoomRedactionEvent {
SyncRoomRedactionEvent::Original(self) SyncRoomRedactionEvent::Original(self)
} }
@ -140,25 +177,85 @@ pub struct RedactedSyncRoomRedactionEvent {
/// A redaction of an event. /// A redaction of an event.
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.room.redaction", kind = MessageLike)] #[ruma_event(type = "m.room.redaction", kind = MessageLike, custom_redacted)]
pub struct RoomRedactionEventContent { pub struct RoomRedactionEventContent {
/// The ID of the event that was redacted.
///
/// This field is required starting from room version 11.
#[serde(skip_serializing_if = "Option::is_none")]
pub redacts: Option<OwnedEventId>,
/// The reason for the redaction, if any. /// The reason for the redaction, if any.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>, pub reason: Option<String>,
} }
impl RoomRedactionEventContent { impl RoomRedactionEventContent {
/// Creates an empty `RoomRedactionEventContent`. /// Creates an empty `RoomRedactionEventContent` according to room versions 1 through 10.
pub fn new() -> Self { pub fn new_v1() -> Self {
Self::default() Self::default()
} }
/// Creates a new `RoomRedactionEventContent` with the given reason. /// Creates a `RoomRedactionEventContent` with the required `redacts` field introduced in room
pub fn with_reason(reason: String) -> Self { /// version 11.
Self { reason: Some(reason) } pub fn new_v11(redacts: OwnedEventId) -> Self {
Self { redacts: Some(redacts), ..Default::default() }
}
/// Add the given reason to this `RoomRedactionEventContent`.
pub fn with_reason(mut self, reason: String) -> Self {
self.reason = Some(reason);
self
} }
} }
impl RedactContent for RoomRedactionEventContent {
type Redacted = RedactedRoomRedactionEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
let redacts = match version {
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
| RoomVersionId::V7
| RoomVersionId::V8
| RoomVersionId::V9
| RoomVersionId::V10 => None,
_ => self.redacts,
};
RedactedRoomRedactionEventContent { redacts }
}
}
/// A redacted redaction event.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct RedactedRoomRedactionEventContent {
/// The ID of the event that was redacted.
///
/// This field is required starting from room version 11.
#[serde(skip_serializing_if = "Option::is_none")]
pub redacts: Option<OwnedEventId>,
}
impl EventContent for RedactedRoomRedactionEventContent {
type EventType = MessageLikeEventType;
fn event_type(&self) -> Self::EventType {
MessageLikeEventType::RoomRedaction
}
}
impl StaticEventContent for RedactedRoomRedactionEventContent {
const TYPE: &'static str = "m.room.redaction";
}
impl RedactedMessageLikeEventContent for RedactedRoomRedactionEventContent {}
impl RoomRedactionEvent { impl RoomRedactionEvent {
/// Returns the `type` of this event. /// Returns the `type` of this event.
pub fn event_type(&self) -> MessageLikeEventType { pub fn event_type(&self) -> MessageLikeEventType {
@ -200,6 +297,19 @@ impl RoomRedactionEvent {
} }
} }
/// Returns the ID of the event that this event redacts, according to the given room version.
///
/// # Panics
///
/// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
/// possible if the event was modified after being deserialized.
pub fn redacts(&self, room_version: &RoomVersionId) -> Option<&EventId> {
match self {
Self::Original(ev) => Some(ev.redacts(room_version)),
Self::Redacted(ev) => ev.content.redacts.as_deref(),
}
}
/// Get the inner `RoomRedactionEvent` if this is an unredacted event. /// Get the inner `RoomRedactionEvent` if this is an unredacted event.
pub fn as_original(&self) -> Option<&OriginalRoomRedactionEvent> { pub fn as_original(&self) -> Option<&OriginalRoomRedactionEvent> {
match self { match self {
@ -209,22 +319,6 @@ impl RoomRedactionEvent {
} }
} }
impl<'de> Deserialize<'de> for RoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?;
if unsigned.and_then(|u| u.redacted_because).is_some() {
Ok(Self::Redacted(from_raw_json_value(&json)?))
} else {
Ok(Self::Original(from_raw_json_value(&json)?))
}
}
}
impl SyncRoomRedactionEvent { impl SyncRoomRedactionEvent {
/// Returns the `type` of this event. /// Returns the `type` of this event.
pub fn event_type(&self) -> MessageLikeEventType { pub fn event_type(&self) -> MessageLikeEventType {
@ -258,6 +352,19 @@ impl SyncRoomRedactionEvent {
} }
} }
/// Returns the ID of the event that this event redacts, according to the given room version.
///
/// # Panics
///
/// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
/// possible if the event was modified after being deserialized.
pub fn redacts(&self, room_version: &RoomVersionId) -> Option<&EventId> {
match self {
Self::Original(ev) => Some(ev.redacts(room_version)),
Self::Redacted(ev) => ev.content.redacts.as_deref(),
}
}
/// Get the inner `SyncRoomRedactionEvent` if this is an unredacted event. /// Get the inner `SyncRoomRedactionEvent` if this is an unredacted event.
pub fn as_original(&self) -> Option<&OriginalSyncRoomRedactionEvent> { pub fn as_original(&self) -> Option<&OriginalSyncRoomRedactionEvent> {
match self { match self {
@ -284,20 +391,36 @@ impl From<RoomRedactionEvent> for SyncRoomRedactionEvent {
} }
} }
impl<'de> Deserialize<'de> for SyncRoomRedactionEvent { impl OriginalRoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> /// Returns the ID of the event that this event redacts, according to the proper `redacts` field
where /// for the given room version.
D: Deserializer<'de>, ///
{ /// If the `redacts` field is not the proper one for the given room version, this falls back to
let json = Box::<RawJsonValue>::deserialize(deserializer)?; /// the one that is available.
let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?; ///
/// # Panics
if unsigned.and_then(|u| u.redacted_because).is_some() { ///
Ok(Self::Redacted(from_raw_json_value(&json)?)) /// Panics if both `redacts` field are `None`, which is only possible if the event was modified
} else { /// after being deserialized.
Ok(Self::Original(from_raw_json_value(&json)?)) pub fn redacts(&self, room_version: &RoomVersionId) -> &EventId {
redacts(room_version, self.redacts.as_deref(), self.content.redacts.as_deref())
} }
} }
impl OriginalSyncRoomRedactionEvent {
/// Returns the ID of the event that this event redacts, according to the proper `redacts` field
/// for the given room version.
///
/// If the `redacts` field is not the proper one for the given room version, this falls back to
/// the one that is available.
///
/// # Panics
///
/// Panics if both `redacts` field are `None`, which is only possible if the event was modified
/// after being deserialized.
pub fn redacts(&self, room_version: &RoomVersionId) -> &EventId {
redacts(room_version, self.redacts.as_deref(), self.content.redacts.as_deref())
}
} }
/// Extra information about a redaction that is not incorporated into the event's hash. /// Extra information about a redaction that is not incorporated into the event's hash.
@ -345,3 +468,41 @@ impl CanBeEmpty for RoomRedactionUnsigned {
self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty() self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty()
} }
} }
/// Returns the value of the proper `redacts` field for the given room version.
///
/// If the `redacts` field is not the proper one for the given room version, this falls back to
/// the one that is available.
///
/// # Panics
///
/// Panics if both `redacts` and `content_redacts` are `None`.
fn redacts<'a>(
room_version: &'_ RoomVersionId,
redacts: Option<&'a EventId>,
content_redacts: Option<&'a EventId>,
) -> &'a EventId {
match room_version {
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
| RoomVersionId::V7
| RoomVersionId::V8
| RoomVersionId::V9
| RoomVersionId::V10 => redacts
.or_else(|| {
error!("Redacts field at event level not available, falling back to the one inside content");
content_redacts
})
.expect("At least one redacts field is set"),
_ => content_redacts
.or_else(|| {
error!("Redacts field inside content not available, falling back to the one at the event level");
redacts
})
.expect("At least one redacts field is set"),
}
}

View File

@ -0,0 +1,105 @@
use serde::{de, Deserialize, Deserializer};
use serde_json::value::RawValue as RawJsonValue;
use super::{
OriginalRoomRedactionEvent, OriginalSyncRoomRedactionEvent, RoomRedactionEvent,
RoomRedactionEventContent, RoomRedactionUnsigned, SyncRoomRedactionEvent,
};
use crate::{
events::RedactionDeHelper, serde::from_raw_json_value, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedRoomId, OwnedUserId,
};
impl<'de> Deserialize<'de> for RoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?;
if unsigned.and_then(|u| u.redacted_because).is_some() {
Ok(Self::Redacted(from_raw_json_value(&json)?))
} else {
Ok(Self::Original(from_raw_json_value(&json)?))
}
}
}
impl<'de> Deserialize<'de> for SyncRoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?;
if unsigned.and_then(|u| u.redacted_because).is_some() {
Ok(Self::Redacted(from_raw_json_value(&json)?))
} else {
Ok(Self::Original(from_raw_json_value(&json)?))
}
}
}
#[derive(Deserialize)]
struct OriginalRoomRedactionEventDeHelper {
content: RoomRedactionEventContent,
redacts: Option<OwnedEventId>,
event_id: OwnedEventId,
sender: OwnedUserId,
origin_server_ts: MilliSecondsSinceUnixEpoch,
room_id: Option<OwnedRoomId>,
#[serde(default)]
unsigned: RoomRedactionUnsigned,
}
impl<'de> Deserialize<'de> for OriginalRoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let OriginalRoomRedactionEventDeHelper {
content,
redacts,
event_id,
sender,
origin_server_ts,
room_id,
unsigned,
} = from_raw_json_value(&json)?;
let Some(room_id) = room_id else { return Err(de::Error::missing_field("room_id")) };
if redacts.is_none() && content.redacts.is_none() {
return Err(de::Error::missing_field("redacts"));
}
Ok(Self { content, redacts, event_id, sender, origin_server_ts, room_id, unsigned })
}
}
impl<'de> Deserialize<'de> for OriginalSyncRoomRedactionEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let OriginalRoomRedactionEventDeHelper {
content,
redacts,
event_id,
sender,
origin_server_ts,
unsigned,
..
} = from_raw_json_value(&json)?;
if redacts.is_none() && content.redacts.is_none() {
return Err(de::Error::missing_field("redacts"));
}
Ok(Self { content, redacts, event_id, sender, origin_server_ts, unsigned })
}
}

View File

@ -22,7 +22,7 @@ fn unsigned() -> JsonValue {
json!({ json!({
"redacted_because": { "redacted_because": {
"type": "m.room.redaction", "type": "m.room.redaction",
"content": RoomRedactionEventContent::with_reason("redacted because".into()), "content": RoomRedactionEventContent::new_v1().with_reason("redacted because".into()),
"redacts": "$h29iv0s8:example.com", "redacts": "$h29iv0s8:example.com",
"event_id": "$h29iv0s8:example.com", "event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1, "origin_server_ts": 1,

View File

@ -5,14 +5,15 @@ use ruma_common::{
room::redaction::{RoomRedactionEvent, RoomRedactionEventContent}, room::redaction::{RoomRedactionEvent, RoomRedactionEventContent},
AnyMessageLikeEvent, AnyMessageLikeEvent,
}, },
owned_event_id,
serde::CanBeEmpty, serde::CanBeEmpty,
MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch, RoomVersionId,
}; };
use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test] #[test]
fn serialize_redaction_content() { fn serialize_redaction_content() {
let content = RoomRedactionEventContent::with_reason("being very unfriendly".into()); let content = RoomRedactionEventContent::new_v1().with_reason("being very unfriendly".into());
let actual = to_json_value(content).unwrap(); let actual = to_json_value(content).unwrap();
let expected = json!({ let expected = json!({
@ -22,13 +23,29 @@ fn serialize_redaction_content() {
assert_eq!(actual, expected); assert_eq!(actual, expected);
} }
#[test]
fn serialize_redaction_content_v11() {
let redacts = owned_event_id!("$abcdef");
let content = RoomRedactionEventContent::new_v11(redacts.clone())
.with_reason("being very unfriendly".into());
let actual = to_json_value(content).unwrap();
let expected = json!({
"redacts": redacts,
"reason": "being very unfriendly"
});
assert_eq!(actual, expected);
}
#[test] #[test]
fn deserialize_redaction() { fn deserialize_redaction() {
let json_data = json!({ let json_data = json!({
"content": { "content": {
"redacts": "$nomorev11:example.com",
"reason": "being very unfriendly" "reason": "being very unfriendly"
}, },
"redacts": "$nomore:example.com", "redacts": "$nomorev1:example.com",
"event_id": "$h29iv0s8:example.com", "event_id": "$h29iv0s8:example.com",
"sender": "@carl:example.com", "sender": "@carl:example.com",
"origin_server_ts": 1, "origin_server_ts": 1,
@ -40,11 +57,32 @@ fn deserialize_redaction() {
from_json_value::<AnyMessageLikeEvent>(json_data), from_json_value::<AnyMessageLikeEvent>(json_data),
Ok(AnyMessageLikeEvent::RoomRedaction(RoomRedactionEvent::Original(ev))) Ok(AnyMessageLikeEvent::RoomRedaction(RoomRedactionEvent::Original(ev)))
); );
assert_eq!(ev.redacts(&RoomVersionId::V1), "$nomorev1:example.com");
assert_eq!(ev.redacts(&RoomVersionId::V11), "$nomorev11:example.com");
assert_eq!(ev.content.redacts.unwrap(), "$nomorev11:example.com");
assert_eq!(ev.content.reason.as_deref(), Some("being very unfriendly")); assert_eq!(ev.content.reason.as_deref(), Some("being very unfriendly"));
assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.event_id, "$h29iv0s8:example.com");
assert_eq!(ev.redacts, "$nomore:example.com"); assert_eq!(ev.redacts.unwrap(), "$nomorev1:example.com");
assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
assert_eq!(ev.room_id, "!roomid:room.com"); assert_eq!(ev.room_id, "!roomid:room.com");
assert_eq!(ev.sender, "@carl:example.com"); assert_eq!(ev.sender, "@carl:example.com");
assert!(ev.unsigned.is_empty()); assert!(ev.unsigned.is_empty());
} }
#[test]
fn deserialize_redaction_missing_redacts() {
let json_data = json!({
"content": {
"reason": "being very unfriendly"
},
"event_id": "$h29iv0s8:example.com",
"sender": "@carl:example.com",
"origin_server_ts": 1,
"room_id": "!roomid:room.com",
"type": "m.room.redaction"
});
from_json_value::<AnyMessageLikeEvent>(json_data).unwrap_err();
}