use std::sync::Arc;
use as_variant::as_variant;
use imbl::Vector;
use matrix_sdk::crypto::types::events::UtdCause;
use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent};
use ruma::{
events::{
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
policy::rule::{
room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent,
user::PolicyRuleUserEventContent,
},
poll::unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
UnstablePollStartEventContent,
},
room::{
aliases::RoomAliasesEventContent,
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, SyncRoomMemberEvent},
message::{
Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
SyncRoomMessageEvent,
},
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
server_acl::RoomServerAclEventContent,
third_party_invite::RoomThirdPartyInviteEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
sticker::{StickerEventContent, SyncStickerEvent},
AnyFullStateEventContent, AnySyncTimelineEvent, FullStateEventContent,
MessageLikeEventType, StateEventType,
},
OwnedDeviceId, OwnedMxcUri, OwnedUserId, RoomVersionId, UserId,
};
use tracing::warn;
use crate::timeline::TimelineItem;
mod message;
pub(crate) mod pinned_events;
mod polls;
pub use pinned_events::RoomPinnedEventsChange;
pub(in crate::timeline) use self::{
message::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
},
polls::ResponseData,
};
pub use self::{
message::{InReplyToDetails, Message, RepliedToEvent},
polls::{PollResult, PollState},
};
#[derive(Clone, Debug)]
pub enum TimelineItemContent {
Message(Message),
RedactedMessage,
Sticker(Sticker),
UnableToDecrypt(EncryptedMessage),
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>,
},
Poll(PollState),
CallInvite,
CallNotify,
}
impl TimelineItemContent {
pub(crate) fn from_latest_event_content(
event: AnySyncTimelineEvent,
power_levels_info: Option<(&UserId, &RoomPowerLevels)>,
) -> Option<TimelineItemContent> {
match is_suitable_for_latest_event(&event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(m) => {
Some(Self::from_suitable_latest_event_content(m))
}
PossibleLatestEvent::YesSticker(s) => {
Some(Self::from_suitable_latest_sticker_content(s))
}
PossibleLatestEvent::YesPoll(poll) => {
Some(Self::from_suitable_latest_poll_event_content(poll))
}
PossibleLatestEvent::YesCallInvite(call_invite) => {
Some(Self::from_suitable_latest_call_invite_content(call_invite))
}
PossibleLatestEvent::YesCallNotify(call_notify) => {
Some(Self::from_suitable_latest_call_notify_content(call_notify))
}
PossibleLatestEvent::NoUnsupportedEventType => {
warn!("Found a state event cached as latest_event! ID={}", event.event_id());
None
}
PossibleLatestEvent::NoUnsupportedMessageLikeType => {
warn!(
"Found an event cached as latest_event, but I don't know how \
to wrap it in a TimelineItemContent. type={}, ID={}",
event.event_type().to_string(),
event.event_id()
);
None
}
PossibleLatestEvent::YesKnockedStateEvent(member) => {
Some(Self::from_suitable_latest_knock_state_event_content(member))
}
PossibleLatestEvent::NoEncrypted => {
warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id());
None
}
}
}
fn from_suitable_latest_event_content(event: &SyncRoomMessageEvent) -> TimelineItemContent {
match event {
SyncRoomMessageEvent::Original(event) => {
let event_content = event.content.clone();
let edit = event
.unsigned
.relations
.replace
.as_ref()
.and_then(|boxed| match &boxed.content.relates_to {
Some(Relation::Replacement(re)) => Some(re.new_content.clone()),
_ => {
warn!("got m.room.message event with an edit without a valid m.replace relation");
None
}
});
let timeline_items = Vector::new();
TimelineItemContent::Message(Message::from_event(
event_content,
edit,
&timeline_items,
))
}
SyncRoomMessageEvent::Redacted(_) => TimelineItemContent::RedactedMessage,
}
}
fn from_suitable_latest_knock_state_event_content(
event: &SyncRoomMemberEvent,
) -> TimelineItemContent {
match event {
SyncRoomMemberEvent::Original(event) => {
let content = event.content.clone();
let prev_content = event.prev_content().cloned();
TimelineItemContent::room_member(
event.state_key.to_owned(),
FullStateEventContent::Original { content, prev_content },
event.sender.to_owned(),
)
}
SyncRoomMemberEvent::Redacted(_) => TimelineItemContent::RedactedMessage,
}
}
fn from_suitable_latest_sticker_content(event: &SyncStickerEvent) -> TimelineItemContent {
match event {
SyncStickerEvent::Original(event) => {
let event_content = event.content.clone();
TimelineItemContent::Sticker(Sticker { content: event_content })
}
SyncStickerEvent::Redacted(_) => TimelineItemContent::RedactedMessage,
}
}
fn from_suitable_latest_poll_event_content(
event: &SyncUnstablePollStartEvent,
) -> TimelineItemContent {
let SyncUnstablePollStartEvent::Original(event) = event else {
return TimelineItemContent::RedactedMessage;
};
let edit =
event.unsigned.relations.replace.as_ref().and_then(|boxed| match &boxed.content {
UnstablePollStartEventContent::Replacement(re) => {
Some(re.relates_to.new_content.clone())
}
_ => {
warn!("got poll event with an edit without a valid m.replace relation");
None
}
});
TimelineItemContent::Poll(PollState::new(
NewUnstablePollStartEventContent::new(event.content.poll_start().clone()),
edit,
))
}
fn from_suitable_latest_call_invite_content(
event: &SyncCallInviteEvent,
) -> TimelineItemContent {
match event {
SyncCallInviteEvent::Original(_) => TimelineItemContent::CallInvite,
SyncCallInviteEvent::Redacted(_) => TimelineItemContent::RedactedMessage,
}
}
fn from_suitable_latest_call_notify_content(
event: &SyncCallNotifyEvent,
) -> TimelineItemContent {
match event {
SyncCallNotifyEvent::Original(_) => TimelineItemContent::CallNotify,
SyncCallNotifyEvent::Redacted(_) => TimelineItemContent::RedactedMessage,
}
}
pub fn as_message(&self) -> Option<&Message> {
as_variant!(self, Self::Message)
}
pub fn as_poll(&self) -> Option<&PollState> {
as_variant!(self, Self::Poll)
}
pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
as_variant!(self, Self::UnableToDecrypt)
}
pub(crate) fn is_redacted(&self) -> bool {
matches!(self, Self::RedactedMessage)
}
pub(crate) fn message(
c: RoomMessageEventContent,
edit: Option<RoomMessageEventContentWithoutRelation>,
timeline_items: &Vector<Arc<TimelineItem>>,
) -> Self {
Self::Message(Message::from_event(c, edit, timeline_items))
}
#[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
match self {
TimelineItemContent::Message(_) => "a message",
TimelineItemContent::RedactedMessage => "a redacted messages",
TimelineItemContent::Sticker(_) => "a sticker",
TimelineItemContent::UnableToDecrypt(_) => "an encrypted message we couldn't decrypt",
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::Poll(_) => "a poll",
TimelineItemContent::CallInvite => "a call invite",
TimelineItemContent::CallNotify => "a call notification",
}
}
pub(crate) fn unable_to_decrypt(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
Self::UnableToDecrypt(EncryptedMessage::from_content(content, cause))
}
pub(crate) fn room_member(
user_id: OwnedUserId,
full_content: FullStateEventContent<RoomMemberEventContent>,
sender: OwnedUserId,
) -> Self {
use ruma::events::room::member::MembershipChange as MChange;
match &full_content {
FullStateEventContent::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),
})
}
}
FullStateEventContent::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
user_id,
content: full_content,
change: None,
}),
}
}
pub(in crate::timeline) fn redact(&self, room_version: &RoomVersionId) -> Self {
match self {
Self::Message(_)
| Self::RedactedMessage
| Self::Sticker(_)
| Self::Poll(_)
| Self::CallInvite
| Self::CallNotify
| Self::UnableToDecrypt(_) => Self::RedactedMessage,
Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(room_version)),
Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
Self::OtherState(ev) => Self::OtherState(ev.redact(room_version)),
Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
}
}
}
#[derive(Clone, Debug)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
sender_key: String,
},
MegolmV1AesSha2 {
#[deprecated = "this field still needs to be sent but should not be used when received"]
#[doc(hidden)] sender_key: String,
#[deprecated = "this field still needs to be sent but should not be used when received"]
#[doc(hidden)] device_id: OwnedDeviceId,
session_id: String,
cause: UtdCause,
},
Unknown,
}
impl EncryptedMessage {
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,
}
}
}
#[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: FullStateEventContent<RoomMemberEventContent>,
pub(in crate::timeline) change: Option<MembershipChange>,
}
impl RoomMembershipChange {
pub fn user_id(&self) -> &UserId {
&self.user_id
}
pub fn content(&self) -> &FullStateEventContent<RoomMemberEventContent> {
&self.content
}
pub fn display_name(&self) -> Option<String> {
if let FullStateEventContent::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 FullStateEventContent::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, room_version: &RoomVersionId) -> Self {
Self {
user_id: self.user_id.clone(),
content: FullStateEventContent::Redacted(self.content.clone().redact(room_version)),
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 AnyOtherFullStateEventContent {
PolicyRuleRoom(FullStateEventContent<PolicyRuleRoomEventContent>),
PolicyRuleServer(FullStateEventContent<PolicyRuleServerEventContent>),
PolicyRuleUser(FullStateEventContent<PolicyRuleUserEventContent>),
RoomAliases(FullStateEventContent<RoomAliasesEventContent>),
RoomAvatar(FullStateEventContent<RoomAvatarEventContent>),
RoomCanonicalAlias(FullStateEventContent<RoomCanonicalAliasEventContent>),
RoomCreate(FullStateEventContent<RoomCreateEventContent>),
RoomEncryption(FullStateEventContent<RoomEncryptionEventContent>),
RoomGuestAccess(FullStateEventContent<RoomGuestAccessEventContent>),
RoomHistoryVisibility(FullStateEventContent<RoomHistoryVisibilityEventContent>),
RoomJoinRules(FullStateEventContent<RoomJoinRulesEventContent>),
RoomName(FullStateEventContent<RoomNameEventContent>),
RoomPinnedEvents(FullStateEventContent<RoomPinnedEventsEventContent>),
RoomPowerLevels(FullStateEventContent<RoomPowerLevelsEventContent>),
RoomServerAcl(FullStateEventContent<RoomServerAclEventContent>),
RoomThirdPartyInvite(FullStateEventContent<RoomThirdPartyInviteEventContent>),
RoomTombstone(FullStateEventContent<RoomTombstoneEventContent>),
RoomTopic(FullStateEventContent<RoomTopicEventContent>),
SpaceChild(FullStateEventContent<SpaceChildEventContent>),
SpaceParent(FullStateEventContent<SpaceParentEventContent>),
#[doc(hidden)]
_Custom { event_type: String },
}
impl AnyOtherFullStateEventContent {
pub(crate) fn with_event_content(content: AnyFullStateEventContent) -> Self {
let event_type = content.event_type();
match content {
AnyFullStateEventContent::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
AnyFullStateEventContent::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
AnyFullStateEventContent::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
AnyFullStateEventContent::RoomAliases(c) => Self::RoomAliases(c),
AnyFullStateEventContent::RoomAvatar(c) => Self::RoomAvatar(c),
AnyFullStateEventContent::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
AnyFullStateEventContent::RoomCreate(c) => Self::RoomCreate(c),
AnyFullStateEventContent::RoomEncryption(c) => Self::RoomEncryption(c),
AnyFullStateEventContent::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
AnyFullStateEventContent::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
AnyFullStateEventContent::RoomJoinRules(c) => Self::RoomJoinRules(c),
AnyFullStateEventContent::RoomName(c) => Self::RoomName(c),
AnyFullStateEventContent::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
AnyFullStateEventContent::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
AnyFullStateEventContent::RoomServerAcl(c) => Self::RoomServerAcl(c),
AnyFullStateEventContent::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
AnyFullStateEventContent::RoomTombstone(c) => Self::RoomTombstone(c),
AnyFullStateEventContent::RoomTopic(c) => Self::RoomTopic(c),
AnyFullStateEventContent::SpaceChild(c) => Self::SpaceChild(c),
AnyFullStateEventContent::SpaceParent(c) => Self::SpaceParent(c),
AnyFullStateEventContent::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::RoomAliases(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, room_version: &RoomVersionId) -> Self {
match self {
Self::PolicyRuleRoom(c) => Self::PolicyRuleRoom(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::PolicyRuleServer(c) => Self::PolicyRuleServer(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::PolicyRuleUser(c) => Self::PolicyRuleUser(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::RoomAliases(c) => {
Self::RoomAliases(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomAvatar(c) => {
Self::RoomAvatar(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(
FullStateEventContent::Redacted(c.clone().redact(room_version)),
),
Self::RoomCreate(c) => {
Self::RoomCreate(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomEncryption(c) => Self::RoomEncryption(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::RoomGuestAccess(c) => Self::RoomGuestAccess(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
FullStateEventContent::Redacted(c.clone().redact(room_version)),
),
Self::RoomJoinRules(c) => {
Self::RoomJoinRules(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomName(c) => {
Self::RoomName(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomPinnedEvents(c) => Self::RoomPinnedEvents(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::RoomPowerLevels(c) => Self::RoomPowerLevels(FullStateEventContent::Redacted(
c.clone().redact(room_version),
)),
Self::RoomServerAcl(c) => {
Self::RoomServerAcl(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
FullStateEventContent::Redacted(c.clone().redact(room_version)),
),
Self::RoomTombstone(c) => {
Self::RoomTombstone(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::RoomTopic(c) => {
Self::RoomTopic(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::SpaceChild(c) => {
Self::SpaceChild(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
Self::SpaceParent(c) => {
Self::SpaceParent(FullStateEventContent::Redacted(c.clone().redact(room_version)))
}
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: AnyOtherFullStateEventContent,
}
impl OtherState {
pub fn state_key(&self) -> &str {
&self.state_key
}
pub fn content(&self) -> &AnyOtherFullStateEventContent {
&self.content
}
fn redact(&self, room_version: &RoomVersionId) -> Self {
Self { state_key: self.state_key.clone(), content: self.content.redact(room_version) }
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_let;
use matrix_sdk_test::ALICE;
use ruma::{
assign,
events::{
room::member::{MembershipState, RoomMemberEventContent},
FullStateEventContent,
},
RoomVersionId,
};
use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
#[test]
fn redact_membership_change() {
let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
user_id: ALICE.to_owned(),
content: FullStateEventContent::Original {
content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
reason: Some("🤬".to_owned()),
}),
prev_content: Some(RoomMemberEventContent::new(MembershipState::Join)),
},
change: Some(MembershipChange::Banned),
});
let redacted = content.redact(&RoomVersionId::V11);
assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
assert_eq!(inner.change, Some(MembershipChange::Banned));
assert_let!(FullStateEventContent::Redacted(inner_content_redacted) = inner.content);
assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
}
}