ruma-events 0.34.0

Serializable types for the events in the Matrix specification.
Documentation
use js_int::Int;
use ruma_common::{
    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId,
    serde::{CanBeEmpty, Raw},
};
use serde::{Deserialize, de::DeserializeOwned};

use super::{
    MessageLikeEventContent, OriginalSyncMessageLikeEvent, PossiblyRedactedStateEventContent,
    relation::{BundledMessageLikeRelations, BundledStateRelations},
    room::redaction::RoomRedactionEventContent,
};
use crate::TimelineEventType;

mod redacted_because_serde;

/// Extra information about a message event that is not incorporated into the event's hash.
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "OriginalSyncMessageLikeEvent<C>: DeserializeOwned")]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct MessageLikeUnsigned<C: MessageLikeEventContent> {
    /// The time in milliseconds that has elapsed since the event was sent.
    ///
    /// This field is generated by the local homeserver, and may be incorrect if the local time on
    /// at least one of the two servers is out of sync, which can cause the age to either be
    /// negative or greater than it actually is.
    pub age: Option<Int>,

    /// The client-supplied transaction ID, if the client being given the event is the same one
    /// which sent it.
    pub transaction_id: Option<OwnedTransactionId>,

    /// [Bundled aggregations] of related child events.
    ///
    /// [Bundled aggregations]: https://spec.matrix.org/v1.18/client-server-api/#aggregations-of-child-events
    #[serde(rename = "m.relations", default)]
    pub relations: BundledMessageLikeRelations<OriginalSyncMessageLikeEvent<C>>,
}

impl<C: MessageLikeEventContent> MessageLikeUnsigned<C> {
    /// Create a new `Unsigned` with fields set to `None`.
    pub fn new() -> Self {
        Self { age: None, transaction_id: None, relations: BundledMessageLikeRelations::default() }
    }
}

impl<C: MessageLikeEventContent> Default for MessageLikeUnsigned<C> {
    fn default() -> Self {
        Self::new()
    }
}

impl<C: MessageLikeEventContent> CanBeEmpty for MessageLikeUnsigned<C> {
    /// Whether this unsigned data is empty (all fields are `None`).
    ///
    /// This method is used to determine whether to skip serializing the `unsigned` field in room
    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
    /// could still have been present but contained none of the known fields.
    fn is_empty(&self) -> bool {
        self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty()
    }
}

/// Extra information about a state event that is not incorporated into the event's hash.
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct StateUnsigned<C: PossiblyRedactedStateEventContent> {
    /// The time in milliseconds that has elapsed since the event was sent.
    ///
    /// This field is generated by the local homeserver, and may be incorrect if the local time on
    /// at least one of the two servers is out of sync, which can cause the age to either be
    /// negative or greater than it actually is.
    pub age: Option<Int>,

    /// The client-supplied transaction ID, if the client being given the event is the same one
    /// which sent it.
    pub transaction_id: Option<OwnedTransactionId>,

    /// Optional previous content of the event.
    pub prev_content: Option<C>,

    /// [Bundled aggregations] of related child events.
    ///
    /// [Bundled aggregations]: https://spec.matrix.org/v1.18/client-server-api/#aggregations-of-child-events
    #[serde(rename = "m.relations", default)]
    pub relations: BundledStateRelations,
}

impl<C: PossiblyRedactedStateEventContent> StateUnsigned<C> {
    /// Create a new `Unsigned` with fields set to `None`.
    pub fn new() -> Self {
        Self { age: None, transaction_id: None, prev_content: None, relations: Default::default() }
    }
}

impl<C: PossiblyRedactedStateEventContent> CanBeEmpty for StateUnsigned<C> {
    /// Whether this unsigned data is empty (all fields are `None`).
    ///
    /// This method is used to determine whether to skip serializing the `unsigned` field in room
    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
    /// could still have been present but contained none of the known fields.
    fn is_empty(&self) -> bool {
        self.age.is_none()
            && self.transaction_id.is_none()
            && self.prev_content.is_none()
            && self.relations.is_empty()
    }
}

impl<C: PossiblyRedactedStateEventContent> Default for StateUnsigned<C> {
    fn default() -> Self {
        Self::new()
    }
}

/// Extra information about a redacted event that is not incorporated into the event's hash.
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct RedactedUnsigned {
    /// The event that redacted this event, if any.
    pub redacted_because: Raw<AnyRedactionEvent>,
}

impl RedactedUnsigned {
    /// Create a new `RedactedUnsigned` with the given redaction event.
    pub fn new(redacted_because: Raw<AnyRedactionEvent>) -> Self {
        Self { redacted_because }
    }
}

/// Any event that can redact another event, i.e. an event that can be found in
/// `unsigned.redacted_because`.
#[derive(Clone, Debug)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum AnyRedactionEvent {
    /// m.room.redaction
    RoomRedaction(UnsignedRoomRedactionEvent),

    /// m.room.member
    #[cfg(feature = "unstable-msc4293")]
    RoomMember(super::room::member::SyncRoomMemberEvent),

    #[doc(hidden)]
    _Custom(CustomRedactionEvent),
}

impl AnyRedactionEvent {
    /// Returns the `type` of this event.
    pub fn event_type(&self) -> TimelineEventType {
        match self {
            Self::RoomRedaction(_) => TimelineEventType::RoomRedaction,
            #[cfg(feature = "unstable-msc4293")]
            Self::RoomMember(_) => TimelineEventType::RoomMember,
            Self::_Custom(e) => TimelineEventType::from(&*e.event_type),
        }
    }

    /// Returns the `origin_server_ts` of this event.
    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
        match self {
            Self::RoomRedaction(e) => e.origin_server_ts,
            #[cfg(feature = "unstable-msc4293")]
            Self::RoomMember(e) => e.origin_server_ts(),
            Self::_Custom(e) => e.origin_server_ts,
        }
    }

    /// Returns the `event_id` of this event.
    pub fn event_id(&self) -> &EventId {
        match self {
            Self::RoomRedaction(e) => &e.event_id,
            #[cfg(feature = "unstable-msc4293")]
            Self::RoomMember(e) => e.event_id(),
            Self::_Custom(e) => &e.event_id,
        }
    }

    /// Returns the `sender` of this event.
    pub fn sender(&self) -> &UserId {
        match self {
            Self::RoomRedaction(e) => &e.sender,
            #[cfg(feature = "unstable-msc4293")]
            Self::RoomMember(e) => e.sender(),
            Self::_Custom(e) => &e.sender,
        }
    }
}

/// An `m.room.redaction` event as found in `unsigned.redacted_because`.
///
/// While servers usually send this with the `redacts` field (unless nested), the ID of the event
/// being redacted is known from context wherever this type is used, so it's not reflected as a
/// field here.
///
/// It is intentionally not possible to create an instance of this type other than through `Clone`
/// or `Deserialize`.
#[derive(Clone, Debug, Deserialize)]
#[non_exhaustive]
pub struct UnsignedRoomRedactionEvent {
    /// Data specific to the event type.
    pub content: RoomRedactionEventContent,

    /// The globally unique event identifier for the user who sent the event.
    pub event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    pub sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    pub origin_server_ts: MilliSecondsSinceUnixEpoch,

    /// Additional key-value pairs not signed by the homeserver.
    #[serde(default)]
    pub unsigned: MessageLikeUnsigned<RoomRedactionEventContent>,
}

/// A custom redaction event.
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct CustomRedactionEvent {
    /// The type of the event
    event_type: Box<str>,

    /// The globally unique event identifier for the user who sent the event.
    event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    origin_server_ts: MilliSecondsSinceUnixEpoch,
}

#[cfg(test)]
mod tests {
    use assert_matches2::assert_matches;
    use js_int::uint;
    use serde_json::{from_value as from_json_value, json};

    use super::AnyRedactionEvent;
    use crate::TimelineEventType;

    #[test]
    fn deserialize_any_redaction_event_room_redaction() {
        let json = json!({
            "type": "m.room.redaction",
            "content": {
                "redacts": "$redactedevent",
            },
            "event_id": "$redactionevent",
            "origin_server_ts": 1,
            "sender": "@carl:example.com",
        });

        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
        assert_eq!(event.event_id(), "$redactionevent");
        assert_eq!(event.origin_server_ts().0, uint!(1));
        assert_eq!(event.sender(), "@carl:example.com");
        assert_eq!(event.event_type(), TimelineEventType::RoomRedaction);
        assert_matches!(event, AnyRedactionEvent::RoomRedaction(_));
    }

    #[test]
    fn deserialize_any_redaction_event_custom() {
        let json = json!({
            "type": "local.dev.custom_type",
            "content": {},
            "event_id": "$redactionevent",
            "origin_server_ts": 1,
            "sender": "@carl:example.com",
        });

        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
        assert_eq!(event.event_id(), "$redactionevent");
        assert_eq!(event.origin_server_ts().0, uint!(1));
        assert_eq!(event.sender(), "@carl:example.com");
        assert_eq!(event.event_type().to_string(), "local.dev.custom_type");
    }
}