use std::sync::Arc;
use as_variant::as_variant;
use matrix_sdk::{Room, deserialized_responses::TimelineEvent};
use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::{
OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId,
events::{
AnyMessageLikeEventContent, AnyStateEventContentChange, Mentions, MessageLikeEventType,
StateEventContentChange, StateEventType,
policy::rule::{
room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent,
user::PolicyRuleUserEventContent,
},
relation::Replacement,
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent},
encryption::RoomEncryptionEventContent,
guest_access::RoomGuestAccessEventContent,
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::RoomJoinRulesEventContent,
member::{Change, RoomMemberEventContent},
message::{MessageType, RoomMessageEventContent},
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
power_levels::RoomPowerLevelsEventContent,
server_acl::RoomServerAclEventContent,
third_party_invite::RoomThirdPartyInviteEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
rtc::notification::CallIntent,
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
sticker::StickerEventContent,
},
html::RemoveReplyFallback,
room_version_rules::RedactionRules,
};
mod live_location;
mod message;
mod msg_like;
pub(super) mod other;
pub(crate) mod pinned_events;
mod polls;
mod reply;
pub use pinned_events::RoomPinnedEventsChange;
pub(in crate::timeline) use self::{
live_location::beacon_info_matches,
message::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
},
};
pub use self::{
live_location::{BeaconInfo, LiveLocationState},
message::Message,
msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary},
other::OtherMessageLike,
polls::{PollResult, PollState},
reply::{EmbeddedEvent, InReplyToDetails},
};
use super::ReactionsByKeyBySender;
use crate::timeline::event_handler::{HandleAggregationKind, TimelineAction};
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum TimelineItemContent {
MsgLike(MsgLikeContent),
MembershipChange(RoomMembershipChange),
ProfileChange(MemberProfileChange),
OtherState(OtherState),
FailedToParseMessageLike {
event_type: MessageLikeEventType,
error: Arc<serde_json::Error>,
},
FailedToParseState {
event_type: StateEventType,
state_key: String,
error: Arc<serde_json::Error>,
},
CallInvite,
RtcNotification {
call_intent: Option<CallIntent>,
declined_by: Vec<OwnedUserId>,
},
}
impl TimelineItemContent {
pub fn event_type_str(&self) -> Option<String> {
match self {
Self::MsgLike(msg) => Some(match &msg.kind {
MsgLikeKind::Message(_) => MessageLikeEventType::RoomMessage.to_string(),
MsgLikeKind::Sticker(_) => MessageLikeEventType::Sticker.to_string(),
MsgLikeKind::Poll(_) => MessageLikeEventType::PollStart.to_string(),
MsgLikeKind::Redacted => return None,
MsgLikeKind::UnableToDecrypt(_) => MessageLikeEventType::RoomEncrypted.to_string(),
MsgLikeKind::Other(other) => other.event_type().to_string(),
MsgLikeKind::LiveLocation(_) => StateEventType::BeaconInfo.to_string(),
}),
Self::MembershipChange(_) | Self::ProfileChange(_) => {
Some(StateEventType::RoomMember.to_string())
}
Self::OtherState(state) => Some(state.content().event_type().to_string()),
Self::FailedToParseMessageLike { event_type, .. } => Some(event_type.to_string()),
Self::FailedToParseState { event_type, .. } => Some(event_type.to_string()),
Self::CallInvite => Some(MessageLikeEventType::CallInvite.to_string()),
Self::RtcNotification { .. } => Some(MessageLikeEventType::RtcNotification.to_string()),
}
}
pub async fn from_event(room: &Room, timeline_event: TimelineEvent) -> Option<Self> {
let raw_event = timeline_event.into_raw();
let deserialized_event = raw_event.deserialize().ok()?;
match TimelineAction::from_event(
deserialized_event,
&raw_event,
room,
None,
None,
None,
None,
)
.await
{
Some(TimelineAction::AddItem { content }) => Some(content),
Some(TimelineAction::HandleAggregation {
kind: HandleAggregationKind::BeaconStop { content },
..
}) => Some(TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::LiveLocation(LiveLocationState::new(content)),
reactions: Default::default(),
thread_root: None,
in_reply_to: None,
thread_summary: None,
})),
Some(TimelineAction::HandleAggregation {
kind: HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
..
}) => {
match TimelineAction::from_content(
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
new_content.msgtype,
)),
None,
None,
None,
) {
TimelineAction::AddItem { content } => Some(content),
_ => None,
}
}
_ => None,
}
}
pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
as_variant!(self, TimelineItemContent::MsgLike)
}
pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
as_variant!(self, Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::LiveLocation(state),
..
}) => state)
}
pub fn as_message(&self) -> Option<&Message> {
as_variant!(self, Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Message(message),
..
}) => message)
}
pub fn is_message(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
}
pub fn as_poll(&self) -> Option<&PollState> {
as_variant!(self, Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Poll(poll_state),
..
}) => poll_state)
}
pub fn is_poll(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
}
pub fn as_sticker(&self) -> Option<&Sticker> {
as_variant!(
self,
Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Sticker(sticker),
..
}) => sticker
)
}
pub fn is_sticker(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
}
pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
as_variant!(
self,
Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
..
}) => encrypted_message
)
}
pub fn is_unable_to_decrypt(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
}
pub fn is_redacted(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
}
pub(crate) fn message(
msgtype: MessageType,
mentions: Option<Mentions>,
reactions: ReactionsByKeyBySender,
thread_root: Option<OwnedEventId>,
in_reply_to: Option<InReplyToDetails>,
thread_summary: Option<ThreadSummary>,
) -> Self {
let remove_reply_fallback =
if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Message(Message::from_event(
msgtype,
mentions,
None,
remove_reply_fallback,
)),
reactions,
thread_root,
in_reply_to,
thread_summary,
})
}
#[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
match self {
TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
TimelineItemContent::MembershipChange(_) => "a membership change",
TimelineItemContent::ProfileChange(_) => "a profile change",
TimelineItemContent::OtherState(_) => "a state event",
TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
TimelineItemContent::CallInvite => "a call invite",
TimelineItemContent::RtcNotification { .. } => "a call notification",
}
}
pub(crate) fn room_member(
user_id: OwnedUserId,
full_content: StateEventContentChange<RoomMemberEventContent>,
sender: OwnedUserId,
) -> Self {
use ruma::events::room::member::MembershipChange as MChange;
match &full_content {
StateEventContentChange::Original { content, prev_content } => {
let membership_change = content.membership_change(
prev_content.as_ref().map(|c| c.details()),
&sender,
&user_id,
);
if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
membership_change
{
Self::ProfileChange(MemberProfileChange {
user_id,
displayname_change: displayname_change.map(|c| Change {
new: c.new.map(ToOwned::to_owned),
old: c.old.map(ToOwned::to_owned),
}),
avatar_url_change: avatar_url_change.map(|c| Change {
new: c.new.map(ToOwned::to_owned),
old: c.old.map(ToOwned::to_owned),
}),
})
} else {
let change = match membership_change {
MChange::None => MembershipChange::None,
MChange::Error => MembershipChange::Error,
MChange::Joined => MembershipChange::Joined,
MChange::Left => MembershipChange::Left,
MChange::Banned => MembershipChange::Banned,
MChange::Unbanned => MembershipChange::Unbanned,
MChange::Kicked => MembershipChange::Kicked,
MChange::Invited => MembershipChange::Invited,
MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
MChange::InvitationRejected => MembershipChange::InvitationRejected,
MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
MChange::Knocked => MembershipChange::Knocked,
MChange::KnockAccepted => MembershipChange::KnockAccepted,
MChange::KnockRetracted => MembershipChange::KnockRetracted,
MChange::KnockDenied => MembershipChange::KnockDenied,
MChange::ProfileChanged { .. } => unreachable!(),
_ => MembershipChange::NotImplemented,
};
Self::MembershipChange(RoomMembershipChange {
user_id,
content: full_content,
change: Some(change),
})
}
}
StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
user_id,
content: full_content,
change: None,
}),
}
}
pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
match self {
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification { .. } => {
TimelineItemContent::MsgLike(MsgLikeContent::redacted())
}
Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
}
}
pub fn thread_root(&self) -> Option<OwnedEventId> {
as_variant!(self, Self::MsgLike)?.thread_root.clone()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
}
pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
match self {
TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
TimelineItemContent::MembershipChange(..)
| TimelineItemContent::ProfileChange(..)
| TimelineItemContent::OtherState(..)
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification { .. } => {
None
}
}
}
pub fn thread_summary(&self) -> Option<ThreadSummary> {
as_variant!(self, Self::MsgLike)?.thread_summary.clone()
}
pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
match self {
TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
TimelineItemContent::MembershipChange(..)
| TimelineItemContent::ProfileChange(..)
| TimelineItemContent::OtherState(..)
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification { .. } => {
None
}
}
}
pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
let mut cloned = self.clone();
if let Some(r) = cloned.reactions_mut() {
*r = reactions;
}
cloned
}
}
#[derive(Clone, Debug)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
sender_key: String,
},
MegolmV1AesSha2 {
#[deprecated = "this field should still be sent but should not be used when received"]
#[doc(hidden)] sender_key: Option<String>,
#[deprecated = "this field should still be sent but should not be used when received"]
#[doc(hidden)] device_id: Option<OwnedDeviceId>,
session_id: String,
cause: UtdCause,
},
Unknown,
}
impl EncryptedMessage {
pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
match content.scheme {
EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
}
#[allow(deprecated)]
EncryptedEventScheme::MegolmV1AesSha2(s) => {
let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
}
_ => Self::Unknown,
}
}
pub(crate) fn session_id(&self) -> Option<&str> {
match self {
EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
EncryptedMessage::Unknown => None,
}
}
}
#[derive(Clone, Debug)]
pub struct Sticker {
pub(in crate::timeline) content: StickerEventContent,
}
impl Sticker {
pub fn content(&self) -> &StickerEventContent {
&self.content
}
}
#[derive(Clone, Debug)]
pub struct RoomMembershipChange {
pub(in crate::timeline) user_id: OwnedUserId,
pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
pub(in crate::timeline) change: Option<MembershipChange>,
}
impl RoomMembershipChange {
pub fn user_id(&self) -> &UserId {
&self.user_id
}
pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
&self.content
}
pub fn display_name(&self) -> Option<String> {
if let StateEventContentChange::Original { content, prev_content } = &self.content {
content
.displayname
.as_ref()
.or_else(|| {
prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
})
.cloned()
} else {
None
}
}
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
if let StateEventContentChange::Original { content, prev_content } = &self.content {
content
.avatar_url
.as_ref()
.or_else(|| {
prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
})
.cloned()
} else {
None
}
}
pub fn change(&self) -> Option<MembershipChange> {
self.change
}
fn redact(&self, rules: &RedactionRules) -> Self {
Self {
user_id: self.user_id.clone(),
content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
change: self.change,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MembershipChange {
None,
Error,
Joined,
Left,
Banned,
Unbanned,
Kicked,
Invited,
KickedAndBanned,
InvitationAccepted,
InvitationRejected,
InvitationRevoked,
Knocked,
KnockAccepted,
KnockRetracted,
KnockDenied,
NotImplemented,
}
#[derive(Clone, Debug)]
pub struct MemberProfileChange {
pub(in crate::timeline) user_id: OwnedUserId,
pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
}
impl MemberProfileChange {
pub fn user_id(&self) -> &UserId {
&self.user_id
}
pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
self.displayname_change.as_ref()
}
pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
self.avatar_url_change.as_ref()
}
fn redact(&self) -> Self {
Self {
user_id: self.user_id.clone(),
displayname_change: None,
avatar_url_change: None,
}
}
}
#[derive(Clone, Debug)]
pub enum AnyOtherStateEventContentChange {
PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
RoomCreate(StateEventContentChange<RoomCreateEventContent>),
RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
RoomName(StateEventContentChange<RoomNameEventContent>),
RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
RoomTopic(StateEventContentChange<RoomTopicEventContent>),
SpaceChild(StateEventContentChange<SpaceChildEventContent>),
SpaceParent(StateEventContentChange<SpaceParentEventContent>),
#[doc(hidden)]
_Custom { event_type: String },
}
impl AnyOtherStateEventContentChange {
pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
let event_type = content.event_type();
match content {
AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
AnyStateEventContentChange::RoomMember(_) => unreachable!(),
_ => Self::_Custom { event_type: event_type.to_string() },
}
}
pub fn event_type(&self) -> StateEventType {
match self {
Self::PolicyRuleRoom(c) => c.event_type(),
Self::PolicyRuleServer(c) => c.event_type(),
Self::PolicyRuleUser(c) => c.event_type(),
Self::RoomAvatar(c) => c.event_type(),
Self::RoomCanonicalAlias(c) => c.event_type(),
Self::RoomCreate(c) => c.event_type(),
Self::RoomEncryption(c) => c.event_type(),
Self::RoomGuestAccess(c) => c.event_type(),
Self::RoomHistoryVisibility(c) => c.event_type(),
Self::RoomJoinRules(c) => c.event_type(),
Self::RoomName(c) => c.event_type(),
Self::RoomPinnedEvents(c) => c.event_type(),
Self::RoomPowerLevels(c) => c.event_type(),
Self::RoomServerAcl(c) => c.event_type(),
Self::RoomThirdPartyInvite(c) => c.event_type(),
Self::RoomTombstone(c) => c.event_type(),
Self::RoomTopic(c) => c.event_type(),
Self::SpaceChild(c) => c.event_type(),
Self::SpaceParent(c) => c.event_type(),
Self::_Custom { event_type } => event_type.as_str().into(),
}
}
fn redact(&self, rules: &RedactionRules) -> Self {
match self {
Self::PolicyRuleRoom(c) => {
Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::PolicyRuleServer(c) => {
Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::PolicyRuleUser(c) => {
Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomAvatar(c) => {
Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomCanonicalAlias(c) => {
Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomCreate(c) => {
Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomEncryption(c) => {
Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomGuestAccess(c) => {
Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
StateEventContentChange::Redacted(c.clone().redact(rules)),
),
Self::RoomJoinRules(c) => {
Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomName(c) => {
Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomPinnedEvents(c) => {
Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomPowerLevels(c) => {
Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomServerAcl(c) => {
Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
StateEventContentChange::Redacted(c.clone().redact(rules)),
),
Self::RoomTombstone(c) => {
Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::RoomTopic(c) => {
Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::SpaceChild(c) => {
Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::SpaceParent(c) => {
Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
}
Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
}
}
}
#[derive(Clone, Debug)]
pub struct OtherState {
pub(in crate::timeline) state_key: String,
pub(in crate::timeline) content: AnyOtherStateEventContentChange,
}
impl OtherState {
pub fn state_key(&self) -> &str {
&self.state_key
}
pub fn content(&self) -> &AnyOtherStateEventContentChange {
&self.content
}
fn redact(&self, rules: &RedactionRules) -> Self {
Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_let;
use matrix_sdk_test::ALICE;
use ruma::{
assign,
events::{
StateEventContentChange,
room::member::{
MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
},
},
room_version_rules::RedactionRules,
};
use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
#[test]
fn redact_membership_change() {
let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
user_id: ALICE.to_owned(),
content: StateEventContentChange::Original {
content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
reason: Some("🤬".to_owned()),
}),
prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
MembershipState::Join,
)),
},
change: Some(MembershipChange::Banned),
});
let redacted = content.redact(&RedactionRules::V11);
assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
assert_eq!(inner.change, Some(MembershipChange::Banned));
assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
}
}