use std::{collections::BTreeMap, fmt, ops::Not, sync::Arc};
use ruma::{
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedUserId,
events::{
AnySyncMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent, AnyToDeviceEvent,
MessageLikeEventType,
},
push::Action,
serde::{
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
SerializeAsRefStr,
},
};
use serde::{Deserialize, Serialize};
use tracing::warn;
#[cfg(target_family = "wasm")]
use wasm_bindgen::prelude::*;
use crate::{
debug::{DebugRawEvent, DebugStructExt},
serde_helpers::{extract_bundled_thread_summary, extract_timestamp},
};
const AUTHENTICITY_NOT_GUARANTEED: &str =
"The authenticity of this encrypted message can't be guaranteed on this device.";
const UNVERIFIED_IDENTITY: &str = "Encrypted by an unverified user.";
const VERIFICATION_VIOLATION: &str =
"Encrypted by a previously-verified user who is no longer verified.";
const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner.";
const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device.";
const MISMATCHED_SENDER: &str = "\
The sender of the event does not match the owner of the device \
that created the Megolm session.";
pub const SENT_IN_CLEAR: &str = "Not encrypted.";
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(from = "OldVerificationStateHelper")]
pub enum VerificationState {
Verified,
Unverified(VerificationLevel),
}
#[derive(Clone, Debug, Deserialize)]
enum OldVerificationStateHelper {
Untrusted,
UnknownDevice,
#[serde(alias = "Trusted")]
Verified,
Unverified(VerificationLevel),
}
impl From<OldVerificationStateHelper> for VerificationState {
fn from(value: OldVerificationStateHelper) -> Self {
match value {
OldVerificationStateHelper::Untrusted => {
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
}
OldVerificationStateHelper::UnknownDevice => {
Self::Unverified(VerificationLevel::None(DeviceLinkProblem::MissingDevice))
}
OldVerificationStateHelper::Verified => Self::Verified,
OldVerificationStateHelper::Unverified(l) => Self::Unverified(l),
}
}
}
impl VerificationState {
pub fn to_shield_state_strict(&self) -> ShieldState {
match self {
VerificationState::Verified => ShieldState::None,
VerificationState::Unverified(level) => match level {
VerificationLevel::UnverifiedIdentity
| VerificationLevel::VerificationViolation
| VerificationLevel::UnsignedDevice => ShieldState::Red {
code: ShieldStateCode::UnverifiedIdentity,
message: UNVERIFIED_IDENTITY,
},
VerificationLevel::None(link) => match link {
DeviceLinkProblem::MissingDevice => ShieldState::Red {
code: ShieldStateCode::UnknownDevice,
message: UNKNOWN_DEVICE,
},
DeviceLinkProblem::InsecureSource => ShieldState::Red {
code: ShieldStateCode::AuthenticityNotGuaranteed,
message: AUTHENTICITY_NOT_GUARANTEED,
},
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
pub fn to_shield_state_lax(&self) -> ShieldState {
match self {
VerificationState::Verified => ShieldState::None,
VerificationState::Unverified(level) => match level {
VerificationLevel::UnverifiedIdentity => {
ShieldState::None
}
VerificationLevel::VerificationViolation => {
ShieldState::Red {
code: ShieldStateCode::VerificationViolation,
message: VERIFICATION_VIOLATION,
}
}
VerificationLevel::UnsignedDevice => {
ShieldState::Red {
code: ShieldStateCode::UnsignedDevice,
message: UNSIGNED_DEVICE,
}
}
VerificationLevel::None(link) => match link {
DeviceLinkProblem::MissingDevice => {
ShieldState::Red {
code: ShieldStateCode::UnknownDevice,
message: UNKNOWN_DEVICE,
}
}
DeviceLinkProblem::InsecureSource => {
ShieldState::Grey {
code: ShieldStateCode::AuthenticityNotGuaranteed,
message: AUTHENTICITY_NOT_GUARANTEED,
}
}
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum VerificationLevel {
UnverifiedIdentity,
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
UnsignedDevice,
None(DeviceLinkProblem),
MismatchedSender,
}
impl fmt::Display for VerificationLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let display = match self {
VerificationLevel::UnverifiedIdentity => "The sender's identity was not verified",
VerificationLevel::VerificationViolation => {
"The sender's identity was previously verified but has changed"
}
VerificationLevel::UnsignedDevice => {
"The sending device was not signed by the user's identity"
}
VerificationLevel::None(..) => "The sending device is not known",
VerificationLevel::MismatchedSender => MISMATCHED_SENDER,
};
write!(f, "{display}")
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum DeviceLinkProblem {
MissingDevice,
InsecureSource,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum ShieldState {
Red {
code: ShieldStateCode,
message: &'static str,
},
Grey {
code: ShieldStateCode,
message: &'static str,
},
None,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(target_family = "wasm", wasm_bindgen)]
pub enum ShieldStateCode {
AuthenticityNotGuaranteed,
UnknownDevice,
UnsignedDevice,
UnverifiedIdentity,
SentInClear,
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
MismatchedSender,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum AlgorithmInfo {
MegolmV1AesSha2 {
curve25519_key: String,
sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
OlmV1Curve25519AesSha2 {
curve25519_public_key_base64: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct EncryptionInfo {
pub sender: OwnedUserId,
pub sender_device: Option<OwnedDeviceId>,
pub algorithm_info: AlgorithmInfo,
pub verification_state: VerificationState,
}
impl EncryptionInfo {
pub fn session_id(&self) -> Option<&str> {
if let AlgorithmInfo::MegolmV1AesSha2 { session_id, .. } = &self.algorithm_info {
session_id.as_deref()
} else {
None
}
}
}
impl<'de> Deserialize<'de> for EncryptionInfo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
pub sender: OwnedUserId,
pub sender_device: Option<OwnedDeviceId>,
pub algorithm_info: AlgorithmInfo,
pub verification_state: VerificationState,
#[serde(rename = "session_id")]
pub old_session_id: Option<String>,
}
let Helper { sender, sender_device, algorithm_info, verification_state, old_session_id } =
Helper::deserialize(deserializer)?;
let algorithm_info = match algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys, session_id } => {
AlgorithmInfo::MegolmV1AesSha2 {
session_id: session_id.or(old_session_id),
curve25519_key,
sender_claimed_keys,
}
}
other => other,
};
Ok(EncryptionInfo { sender, sender_device, algorithm_info, verification_state })
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ThreadSummary {
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_reply: Option<OwnedEventId>,
pub num_replies: u32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum ThreadSummaryStatus {
#[default]
Unknown,
None,
Some(ThreadSummary),
}
impl ThreadSummaryStatus {
pub fn from_opt(summary: Option<ThreadSummary>) -> Self {
match summary {
None => ThreadSummaryStatus::None,
Some(summary) => ThreadSummaryStatus::Some(summary),
}
}
fn is_unknown(&self) -> bool {
matches!(self, ThreadSummaryStatus::Unknown)
}
pub fn summary(&self) -> Option<&ThreadSummary> {
match self {
ThreadSummaryStatus::Unknown | ThreadSummaryStatus::None => None,
ThreadSummaryStatus::Some(thread_summary) => Some(thread_summary),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct TimelineEvent {
pub kind: TimelineEventKind,
pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
#[serde(skip_serializing_if = "skip_serialize_push_actions")]
push_actions: Option<Vec<Action>>,
#[serde(default, skip_serializing_if = "ThreadSummaryStatus::is_unknown")]
pub thread_summary: ThreadSummaryStatus,
#[serde(skip)]
pub bundled_latest_thread_event: Option<Box<TimelineEvent>>,
}
fn skip_serialize_push_actions(push_actions: &Option<Vec<Action>>) -> bool {
push_actions.as_ref().is_none_or(|v| v.is_empty())
}
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Send for TimelineEvent {}
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Sync for TimelineEvent {}
#[cfg(feature = "test-send-sync")]
#[test]
fn test_send_sync_for_sync_timeline_event() {
fn assert_send_sync<T: crate::SendOutsideWasm + crate::SyncOutsideWasm>() {}
assert_send_sync::<TimelineEvent>();
}
impl TimelineEvent {
pub fn from_plaintext(event: Raw<AnySyncTimelineEvent>) -> Self {
Self::from_plaintext_with_max_timestamp(event, MilliSecondsSinceUnixEpoch::now())
}
pub fn from_plaintext_with_max_timestamp(
event: Raw<AnySyncTimelineEvent>,
max_timestamp: MilliSecondsSinceUnixEpoch,
) -> Self {
Self::new(TimelineEventKind::PlainText { event }, None, max_timestamp)
}
pub fn from_decrypted(
decrypted: DecryptedRoomEvent,
push_actions: Option<Vec<Action>>,
) -> Self {
Self::from_decrypted_with_max_timestamp(
decrypted,
push_actions,
MilliSecondsSinceUnixEpoch::now(),
)
}
pub fn from_decrypted_with_max_timestamp(
decrypted: DecryptedRoomEvent,
push_actions: Option<Vec<Action>>,
max_timestamp: MilliSecondsSinceUnixEpoch,
) -> Self {
Self::new(TimelineEventKind::Decrypted(decrypted), push_actions, max_timestamp)
}
pub fn from_utd(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
Self::from_utd_with_max_timestamp(event, utd_info, MilliSecondsSinceUnixEpoch::now())
}
pub fn from_utd_with_max_timestamp(
event: Raw<AnySyncTimelineEvent>,
utd_info: UnableToDecryptInfo,
max_timestamp: MilliSecondsSinceUnixEpoch,
) -> Self {
Self::new(TimelineEventKind::UnableToDecrypt { event, utd_info }, None, max_timestamp)
}
fn new(
kind: TimelineEventKind,
push_actions: Option<Vec<Action>>,
max_timestamp: MilliSecondsSinceUnixEpoch,
) -> Self {
let raw = kind.raw();
let (thread_summary, latest_thread_event) = extract_bundled_thread_summary(raw);
let bundled_latest_thread_event =
Self::from_bundled_latest_event(&kind, latest_thread_event, max_timestamp);
let timestamp = extract_timestamp(raw, max_timestamp);
Self { kind, push_actions, timestamp, thread_summary, bundled_latest_thread_event }
}
pub fn to_decrypted(
&self,
decrypted: DecryptedRoomEvent,
push_actions: Option<Vec<Action>>,
) -> Self {
debug_assert!(
matches!(self.kind, TimelineEventKind::Decrypted(_)).not(),
"`TimelineEvent::to_decrypted` has been called on an already decrypted `TimelineEvent`."
);
Self {
kind: TimelineEventKind::Decrypted(decrypted),
timestamp: self.timestamp,
push_actions,
thread_summary: self.thread_summary.clone(),
bundled_latest_thread_event: self.bundled_latest_thread_event.clone(),
}
}
pub fn to_utd(&self, utd_info: UnableToDecryptInfo) -> Self {
debug_assert!(
matches!(self.kind, TimelineEventKind::UnableToDecrypt { .. }).not(),
"`TimelineEvent::to_utd` has been called on an already UTD `TimelineEvent`."
);
Self {
kind: TimelineEventKind::UnableToDecrypt { event: self.raw().clone(), utd_info },
timestamp: self.timestamp,
push_actions: None,
thread_summary: self.thread_summary.clone(),
bundled_latest_thread_event: self.bundled_latest_thread_event.clone(),
}
}
fn from_bundled_latest_event(
kind: &TimelineEventKind,
latest_event: Option<Raw<AnySyncMessageLikeEvent>>,
max_timestamp: MilliSecondsSinceUnixEpoch,
) -> Option<Box<Self>> {
let latest_event = latest_event?;
match kind {
TimelineEventKind::Decrypted(decrypted) => {
if let Some(unsigned_decryption_result) =
decrypted.unsigned_encryption_info.as_ref().and_then(|unsigned_map| {
unsigned_map.get(&UnsignedEventLocation::RelationsThreadLatestEvent)
})
{
match unsigned_decryption_result {
UnsignedDecryptionResult::Decrypted(encryption_info) => {
return Some(Box::new(
TimelineEvent::from_decrypted_with_max_timestamp(
DecryptedRoomEvent {
event: latest_event.cast_unchecked(),
encryption_info: encryption_info.clone(),
unsigned_encryption_info: None,
},
None,
max_timestamp,
),
));
}
UnsignedDecryptionResult::UnableToDecrypt(utd_info) => {
return Some(Box::new(TimelineEvent::from_utd_with_max_timestamp(
latest_event.cast(),
utd_info.clone(),
max_timestamp,
)));
}
}
}
}
TimelineEventKind::UnableToDecrypt { .. } | TimelineEventKind::PlainText { .. } => {
}
}
match latest_event.get_field::<MessageLikeEventType>("type") {
Ok(None) => {
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
warn!(
?event_id,
"couldn't deserialize bundled latest thread event: missing `type` field \
in bundled latest thread event"
);
None
}
Ok(Some(MessageLikeEventType::RoomEncrypted)) => {
Some(Box::new(TimelineEvent::from_utd_with_max_timestamp(
latest_event.cast(),
UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::Unknown,
},
max_timestamp,
)))
}
Ok(_) => Some(Box::new(TimelineEvent::from_plaintext_with_max_timestamp(
latest_event.cast(),
max_timestamp,
))),
Err(err) => {
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
warn!(?event_id, "couldn't deserialize bundled latest thread event's type: {err}");
None
}
}
}
pub fn push_actions(&self) -> Option<&[Action]> {
self.push_actions.as_deref()
}
pub fn set_push_actions(&mut self, push_actions: Vec<Action>) {
self.push_actions = Some(push_actions);
}
pub fn event_id(&self) -> Option<OwnedEventId> {
self.kind.event_id()
}
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
self.kind.raw()
}
pub fn replace_raw(&mut self, replacement: Raw<AnyTimelineEvent>) {
match &mut self.kind {
TimelineEventKind::Decrypted(decrypted) => decrypted.event = replacement,
TimelineEventKind::UnableToDecrypt { event, .. }
| TimelineEventKind::PlainText { event } => {
*event = replacement.cast();
}
}
}
pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.timestamp.or_else(|| {
warn!("`TimelineEvent::timestamp` is parsing the raw event to extract the `timestamp`");
extract_timestamp(self.raw(), MilliSecondsSinceUnixEpoch::now())
})
}
pub fn timestamp_raw(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.timestamp
}
pub fn encryption_info(&self) -> Option<&Arc<EncryptionInfo>> {
self.kind.encryption_info()
}
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
self.kind.into_raw()
}
}
impl<'de> Deserialize<'de> for TimelineEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde_json::{Map, Value};
let value = Map::<String, Value>::deserialize(deserializer)?;
if value.contains_key("event") {
let v0: SyncTimelineEventDeserializationHelperV0 =
serde_json::from_value(Value::Object(value)).map_err(|e| {
serde::de::Error::custom(format!(
"Unable to deserialize V0-format TimelineEvent: {e}",
))
})?;
Ok(v0.into())
}
else {
let v1: SyncTimelineEventDeserializationHelperV1 =
serde_json::from_value(Value::Object(value)).map_err(|e| {
serde::de::Error::custom(format!(
"Unable to deserialize V1-format TimelineEvent: {e}",
))
})?;
Ok(v1.into())
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub enum TimelineEventKind {
Decrypted(DecryptedRoomEvent),
UnableToDecrypt {
event: Raw<AnySyncTimelineEvent>,
utd_info: UnableToDecryptInfo,
},
PlainText {
event: Raw<AnySyncTimelineEvent>,
},
}
impl TimelineEventKind {
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
match self {
TimelineEventKind::Decrypted(d) => d.event.cast_ref(),
TimelineEventKind::UnableToDecrypt { event, .. } => event,
TimelineEventKind::PlainText { event } => event,
}
}
pub fn event_id(&self) -> Option<OwnedEventId> {
self.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
}
pub fn is_utd(&self) -> bool {
matches!(self, TimelineEventKind::UnableToDecrypt { .. })
}
pub fn encryption_info(&self) -> Option<&Arc<EncryptionInfo>> {
match self {
TimelineEventKind::Decrypted(d) => Some(&d.encryption_info),
TimelineEventKind::UnableToDecrypt { .. } | TimelineEventKind::PlainText { .. } => None,
}
}
pub fn unsigned_encryption_map(
&self,
) -> Option<&BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>> {
match self {
TimelineEventKind::Decrypted(d) => d.unsigned_encryption_info.as_ref(),
TimelineEventKind::UnableToDecrypt { .. } | TimelineEventKind::PlainText { .. } => None,
}
}
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
match self {
TimelineEventKind::Decrypted(d) => d.event.cast(),
TimelineEventKind::UnableToDecrypt { event, .. } => event,
TimelineEventKind::PlainText { event } => event,
}
}
pub fn session_id(&self) -> Option<&str> {
match self {
TimelineEventKind::Decrypted(decrypted_room_event) => {
decrypted_room_event.encryption_info.session_id()
}
TimelineEventKind::UnableToDecrypt { utd_info, .. } => utd_info.session_id.as_deref(),
TimelineEventKind::PlainText { .. } => None,
}
}
pub fn event_type(&self) -> Option<String> {
self.raw().get_field("type").ok().flatten()
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for TimelineEventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::PlainText { event } => f
.debug_struct("TimelineEventDecryptionResult::PlainText")
.field("event", &DebugRawEvent(event))
.finish(),
Self::UnableToDecrypt { event, utd_info } => f
.debug_struct("TimelineEventDecryptionResult::UnableToDecrypt")
.field("event", &DebugRawEvent(event))
.field("utd_info", &utd_info)
.finish(),
Self::Decrypted(decrypted) => {
f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish()
}
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DecryptedRoomEvent {
pub event: Raw<AnyTimelineEvent>,
pub encryption_info: Arc<EncryptionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for DecryptedRoomEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info } = self;
f.debug_struct("DecryptedRoomEvent")
.field("event", &DebugRawEvent(event))
.field("encryption_info", encryption_info)
.maybe_field("unsigned_encryption_info", unsigned_encryption_info)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum UnsignedEventLocation {
RelationsReplace,
RelationsThreadLatestEvent,
}
impl UnsignedEventLocation {
pub fn find_mut<'a>(&self, unsigned: &'a mut JsonObject) -> Option<&'a mut serde_json::Value> {
let relations = unsigned.get_mut("m.relations")?.as_object_mut()?;
match self {
Self::RelationsReplace => relations.get_mut("m.replace"),
Self::RelationsThreadLatestEvent => {
relations.get_mut("m.thread")?.as_object_mut()?.get_mut("latest_event")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnsignedDecryptionResult {
Decrypted(Arc<EncryptionInfo>),
UnableToDecrypt(UnableToDecryptInfo),
}
impl UnsignedDecryptionResult {
pub fn encryption_info(&self) -> Option<&Arc<EncryptionInfo>> {
match self {
Self::Decrypted(info) => Some(info),
Self::UnableToDecrypt(_) => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnableToDecryptInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")]
pub reason: UnableToDecryptReason,
}
fn unknown_utd_reason() -> UnableToDecryptReason {
UnableToDecryptReason::Unknown
}
pub fn deserialize_utd_reason<'de, D>(d: D) -> Result<UnableToDecryptReason, D::Error>
where
D: serde::Deserializer<'de>,
{
let v: serde_json::Value = Deserialize::deserialize(d)?;
if v.as_str().is_some_and(|s| s == "MissingMegolmSession") {
return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None });
}
serde_json::from_value::<UnableToDecryptReason>(v).map_err(serde::de::Error::custom)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UnableToDecryptReason {
#[doc(hidden)]
Unknown,
MalformedEncryptedEvent,
MissingMegolmSession {
withheld_code: Option<WithheldCode>,
},
UnknownMegolmMessageIndex,
MegolmDecryptionFailure,
PayloadDeserializationFailure,
MismatchedIdentityKeys,
SenderIdentityNotTrusted(VerificationLevel),
#[cfg(feature = "experimental-encrypted-state-events")]
StateKeyVerificationFailed,
}
impl UnableToDecryptReason {
pub fn is_missing_room_key(&self) -> bool {
matches!(
self,
Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex
)
}
}
#[derive(
Clone,
PartialEq,
Eq,
Hash,
AsStrAsRefStr,
AsRefStr,
FromString,
DebugAsRefStr,
SerializeAsRefStr,
DeserializeFromCowStr,
)]
pub enum WithheldCode {
#[ruma_enum(rename = "m.blacklisted")]
Blacklisted,
#[ruma_enum(rename = "m.unverified")]
Unverified,
#[ruma_enum(rename = "m.unauthorised")]
Unauthorised,
#[ruma_enum(rename = "m.unavailable")]
Unavailable,
#[ruma_enum(rename = "m.no_olm")]
NoOlm,
#[ruma_enum(rename = "io.element.msc4268.history_not_shared", alias = "m.history_not_shared")]
HistoryNotShared,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl fmt::Display for WithheldCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let string = match self {
WithheldCode::Blacklisted => "The sender has blocked you.",
WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.",
WithheldCode::Unauthorised => "You are not authorised to read the message.",
WithheldCode::Unavailable => "The requested key was not found.",
WithheldCode::NoOlm => "Unable to establish a secure channel.",
WithheldCode::HistoryNotShared => "The sender disabled sharing encrypted history.",
_ => self.as_str(),
};
f.write_str(string)
}
}
#[doc(hidden)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrivOwnedStr(pub Box<str>);
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for PrivOwnedStr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Deserialize)]
struct SyncTimelineEventDeserializationHelperV1 {
kind: TimelineEventKind,
#[serde(default)]
timestamp: Option<MilliSecondsSinceUnixEpoch>,
#[serde(default)]
push_actions: Vec<Action>,
#[serde(default)]
thread_summary: ThreadSummaryStatus,
}
impl From<SyncTimelineEventDeserializationHelperV1> for TimelineEvent {
fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self {
let SyncTimelineEventDeserializationHelperV1 {
kind,
timestamp,
push_actions,
thread_summary,
} = value;
TimelineEvent {
kind,
timestamp,
push_actions: Some(push_actions),
thread_summary,
bundled_latest_thread_event: None,
}
}
}
#[derive(Deserialize)]
struct SyncTimelineEventDeserializationHelperV0 {
event: Raw<AnySyncTimelineEvent>,
encryption_info: Option<Arc<EncryptionInfo>>,
#[serde(default)]
push_actions: Vec<Action>,
unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
impl From<SyncTimelineEventDeserializationHelperV0> for TimelineEvent {
fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self {
let SyncTimelineEventDeserializationHelperV0 {
event,
encryption_info,
push_actions,
unsigned_encryption_info,
} = value;
let timestamp = None;
let kind = match encryption_info {
Some(encryption_info) => {
TimelineEventKind::Decrypted(DecryptedRoomEvent {
event: event.cast_unchecked(),
encryption_info,
unsigned_encryption_info,
})
}
None => TimelineEventKind::PlainText { event },
};
TimelineEvent {
kind,
timestamp,
push_actions: Some(push_actions),
thread_summary: ThreadSummaryStatus::Unknown,
bundled_latest_thread_event: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ToDeviceUnableToDecryptReason {
DecryptionFailure,
UnverifiedSenderDevice,
NoOlmMachine,
EncryptionIsDisabled,
}
#[derive(Clone, Debug)]
pub struct ToDeviceUnableToDecryptInfo {
pub reason: ToDeviceUnableToDecryptReason,
}
#[derive(Clone, Debug)]
pub enum ProcessedToDeviceEvent {
Decrypted {
raw: Raw<AnyToDeviceEvent>,
encryption_info: EncryptionInfo,
},
UnableToDecrypt {
encrypted_event: Raw<AnyToDeviceEvent>,
utd_info: ToDeviceUnableToDecryptInfo,
},
PlainText(Raw<AnyToDeviceEvent>),
Invalid(Raw<AnyToDeviceEvent>),
}
impl ProcessedToDeviceEvent {
pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw.clone(),
ProcessedToDeviceEvent::UnableToDecrypt { encrypted_event, .. } => {
encrypted_event.clone()
}
ProcessedToDeviceEvent::PlainText(event) => event.clone(),
ProcessedToDeviceEvent::Invalid(event) => event.clone(),
}
}
pub fn as_raw(&self) -> &Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw,
ProcessedToDeviceEvent::UnableToDecrypt { encrypted_event, .. } => encrypted_event,
ProcessedToDeviceEvent::PlainText(event) => event,
ProcessedToDeviceEvent::Invalid(event) => event,
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, sync::Arc};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use insta::{assert_json_snapshot, with_settings};
use ruma::{
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, UInt, device_id, event_id,
events::room::message::RoomMessageEventContent, serde::Raw, user_id,
};
use serde::Deserialize;
use serde_json::json;
use super::{
AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, ShieldState,
ShieldStateCode, TimelineEvent, TimelineEventKind, UnableToDecryptInfo,
UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel,
VerificationState, WithheldCode,
};
use crate::deserialized_responses::{ThreadSummary, ThreadSummaryStatus};
fn example_event() -> serde_json::Value {
json!({
"content": RoomMessageEventContent::text_plain("secret"),
"type": "m.room.message",
"event_id": "$xxxxx:example.org",
"room_id": "!someroom:example.com",
"origin_server_ts": 2189,
"sender": "@carl:example.com",
})
}
#[test]
fn sync_timeline_debug_content() {
let room_event =
TimelineEvent::from_plaintext(Raw::new(&example_event()).unwrap().cast_unchecked());
let debug_s = format!("{room_event:?}");
assert!(
!debug_s.contains("secret"),
"Debug representation contains event content!\n{debug_s}"
);
}
#[test]
fn old_verification_state_to_new_migration() {
#[derive(Deserialize)]
struct State {
state: VerificationState,
}
let state = json!({
"state": "Trusted",
});
let deserialized: State =
serde_json::from_value(state).expect("We can deserialize the old trusted value");
assert_eq!(deserialized.state, VerificationState::Verified);
let state = json!({
"state": "UnknownDevice",
});
let deserialized: State =
serde_json::from_value(state).expect("We can deserialize the old unknown device value");
assert_eq!(
deserialized.state,
VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::MissingDevice
))
);
let state = json!({
"state": "Untrusted",
});
let deserialized: State =
serde_json::from_value(state).expect("We can deserialize the old trusted value");
assert_eq!(
deserialized.state,
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
);
}
#[test]
fn test_verification_level_deserializes() {
#[derive(Deserialize)]
struct Container {
verification_level: VerificationLevel,
}
let container = json!({ "verification_level": "VerificationViolation" });
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
}
#[test]
fn test_verification_level_deserializes_from_old_previously_verified_value() {
#[derive(Deserialize)]
struct Container {
verification_level: VerificationLevel,
}
let container = json!({ "verification_level": "PreviouslyVerified" });
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
}
#[test]
fn test_shield_state_code_deserializes() {
#[derive(Deserialize)]
struct Container {
shield_state_code: ShieldStateCode,
}
let container = json!({ "shield_state_code": "VerificationViolation" });
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
}
#[test]
fn test_shield_state_code_deserializes_from_old_previously_verified_value() {
#[derive(Deserialize)]
struct Container {
shield_state_code: ShieldStateCode,
}
let container = json!({ "shield_state_code": "PreviouslyVerified" });
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
}
#[test]
fn sync_timeline_event_serialisation() {
let room_event = TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event: Raw::new(&example_event()).unwrap().cast_unchecked(),
encryption_info: Arc::new(EncryptionInfo {
sender: user_id!("@sender:example.com").to_owned(),
sender_device: None,
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "xxx".to_owned(),
sender_claimed_keys: Default::default(),
session_id: Some("xyz".to_owned()),
},
verification_state: VerificationState::Verified,
}),
unsigned_encryption_info: Some(BTreeMap::from([(
UnsignedEventLocation::RelationsReplace,
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
session_id: Some("xyz".to_owned()),
reason: UnableToDecryptReason::MalformedEncryptedEvent,
}),
)])),
}),
timestamp: Some(MilliSecondsSinceUnixEpoch(UInt::new_saturating(2189))),
push_actions: Default::default(),
thread_summary: ThreadSummaryStatus::Unknown,
bundled_latest_thread_event: None,
};
let serialized = serde_json::to_value(&room_event).unwrap();
assert_eq!(
serialized,
json!({
"kind": {
"Decrypted": {
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {},
"session_id": "xyz",
}
},
"verification_state": "Verified",
},
"unsigned_encryption_info": {
"RelationsReplace": {"UnableToDecrypt": {
"session_id": "xyz",
"reason": "MalformedEncryptedEvent",
}}
}
}
},
"timestamp": 2189,
})
);
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
assert_eq!(event.timestamp(), Some(MilliSecondsSinceUnixEpoch(UInt::new_saturating(2189))));
assert_eq!(event.timestamp(), event.timestamp_raw());
let serialized = json!({
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
},
});
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { session_id: None, .. }
);
assert_eq!(event.timestamp(), Some(MilliSecondsSinceUnixEpoch(UInt::new_saturating(2189))));
assert!(event.timestamp_raw().is_none());
let serialized = json!({
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
},
"unsigned_encryption_info": {
"RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}}
}
});
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
assert_eq!(event.timestamp(), Some(MilliSecondsSinceUnixEpoch(UInt::new_saturating(2189))));
assert!(event.timestamp_raw().is_none());
assert_matches!(event.kind, TimelineEventKind::Decrypted(decrypted) => {
assert_matches!(decrypted.unsigned_encryption_info, Some(map) => {
assert_eq!(map.len(), 1);
let (location, result) = map.into_iter().next().unwrap();
assert_eq!(location, UnsignedEventLocation::RelationsReplace);
assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => {
assert_eq!(utd_info.session_id, Some("xyz".to_owned()));
assert_eq!(utd_info.reason, UnableToDecryptReason::Unknown);
})
});
});
}
#[test]
fn test_creating_or_deserializing_an_event_extracts_summary() {
let event = json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello, world!",
},
"unsigned": {
"m.relations": {
"m.thread": {
"latest_event": {
"event_id": "$latest_event:example.com",
"type": "m.room.message",
"sender": "@bob:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello to you too!",
"msgtype": "m.text",
}
},
"count": 2,
"current_user_participated": true,
}
}
}
});
let raw = Raw::new(&event).unwrap().cast_unchecked();
let timeline_event = TimelineEvent::from_plaintext(raw);
assert_matches!(timeline_event.thread_summary, ThreadSummaryStatus::Some(ThreadSummary { num_replies, latest_reply }) => {
assert_eq!(num_replies, 2);
assert_eq!(latest_reply.as_deref(), Some(event_id!("$latest_event:example.com")));
});
assert!(timeline_event.bundled_latest_thread_event.is_some());
let serialized_timeline_item = json!({
"kind": {
"PlainText": {
"event": event
}
}
});
let timeline_event: TimelineEvent =
serde_json::from_value(serialized_timeline_item).unwrap();
assert_matches!(timeline_event.thread_summary, ThreadSummaryStatus::Unknown);
assert!(timeline_event.bundled_latest_thread_event.is_none());
}
#[test]
fn sync_timeline_event_deserialisation_migration_for_withheld() {
let serialized = json!({
"kind": {
"UnableToDecrypt": {
"event": {
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk",
"device_id": "SKCGPNUWAU",
"sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0",
"session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs"
},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message"
},
"utd_info": {
"reason": "MissingMegolmSession",
"session_id": "session000"
}
}
}
});
let result = serde_json::from_value(serialized);
assert!(result.is_ok());
let event: TimelineEvent = result.unwrap();
assert_matches!(
event.kind,
TimelineEventKind::UnableToDecrypt { utd_info, .. }=> {
assert_matches!(
utd_info.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None }
);
}
)
}
#[test]
fn unable_to_decrypt_info_migration_for_withheld() {
let old_format = json!({
"reason": "MissingMegolmSession",
"session_id": "session000"
});
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(old_format).unwrap();
let session_id = Some("session000".to_owned());
assert_eq!(deserialized.session_id, session_id);
assert_eq!(
deserialized.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
);
let new_format = json!({
"session_id": "session000",
"reason": {
"MissingMegolmSession": {
"withheld_code": null
}
}
});
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(new_format).unwrap();
assert_eq!(
deserialized.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
);
assert_eq!(deserialized.session_id, session_id);
}
#[test]
fn unable_to_decrypt_reason_is_missing_room_key() {
let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None };
assert!(reason.is_missing_room_key());
let reason = UnableToDecryptReason::MissingMegolmSession {
withheld_code: Some(WithheldCode::Blacklisted),
};
assert!(!reason.is_missing_room_key());
let reason = UnableToDecryptReason::UnknownMegolmMessageIndex;
assert!(reason.is_missing_room_key());
}
#[test]
fn snapshot_test_verification_level() {
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(VerificationLevel::VerificationViolation);
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
});
}
#[test]
fn snapshot_test_verification_states() {
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
assert_json_snapshot!(VerificationState::Unverified(
VerificationLevel::VerificationViolation
));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::InsecureSource,
)));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::MissingDevice,
)));
assert_json_snapshot!(VerificationState::Verified);
});
}
#[test]
fn snapshot_test_shield_states() {
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(ShieldState::None);
assert_json_snapshot!(ShieldState::Red {
code: ShieldStateCode::UnverifiedIdentity,
message: "a message"
});
assert_json_snapshot!(ShieldState::Grey {
code: ShieldStateCode::AuthenticityNotGuaranteed,
message: "authenticity of this message cannot be guaranteed",
});
});
}
#[test]
fn snapshot_test_shield_codes() {
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
assert_json_snapshot!(ShieldStateCode::SentInClear);
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
});
}
#[test]
fn snapshot_test_algorithm_info() {
let mut map = BTreeMap::new();
map.insert(DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned());
map.insert(DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned());
let info = AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "curvecurvecurve".into(),
sender_claimed_keys: BTreeMap::from([
(DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned()),
(DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned()),
]),
session_id: None,
};
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(info)
});
}
#[test]
fn test_encryption_info_migration() {
let old_format = json!({
"sender": "@alice:localhost",
"sender_device": "ABCDEFGH",
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "curvecurvecurve",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
"session_id": "mysessionid76"
});
let deserialized = serde_json::from_value::<EncryptionInfo>(old_format).unwrap();
let expected_session_id = Some("mysessionid76".to_owned());
assert_let!(
AlgorithmInfo::MegolmV1AesSha2 { session_id, .. } = deserialized.algorithm_info.clone()
);
assert_eq!(session_id, expected_session_id);
assert_json_snapshot!(deserialized);
}
#[test]
fn snapshot_test_encryption_info() {
let info = EncryptionInfo {
sender: user_id!("@alice:localhost").to_owned(),
sender_device: Some(device_id!("ABCDEFGH").to_owned()),
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "curvecurvecurve".into(),
sender_claimed_keys: Default::default(),
session_id: Some("mysessionid76".to_owned()),
},
verification_state: VerificationState::Verified,
};
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
assert_json_snapshot!(info)
})
}
#[test]
fn snapshot_test_sync_timeline_event() {
let room_event = TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event: Raw::new(&example_event()).unwrap().cast_unchecked(),
encryption_info: Arc::new(EncryptionInfo {
sender: user_id!("@sender:example.com").to_owned(),
sender_device: Some(device_id!("ABCDEFGHIJ").to_owned()),
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "xxx".to_owned(),
sender_claimed_keys: BTreeMap::from([
(
DeviceKeyAlgorithm::Ed25519,
"I3YsPwqMZQXHkSQbjFNEs7b529uac2xBpI83eN3LUXo".to_owned(),
),
(
DeviceKeyAlgorithm::Curve25519,
"qzdW3F5IMPFl0HQgz5w/L5Oi/npKUFn8Um84acIHfPY".to_owned(),
),
]),
session_id: Some("mysessionid112".to_owned()),
},
verification_state: VerificationState::Verified,
}),
unsigned_encryption_info: Some(BTreeMap::from([(
UnsignedEventLocation::RelationsThreadLatestEvent,
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
session_id: Some("xyz".to_owned()),
reason: UnableToDecryptReason::MissingMegolmSession {
withheld_code: Some(WithheldCode::Unverified),
},
}),
)])),
}),
timestamp: Some(MilliSecondsSinceUnixEpoch(UInt::new_saturating(2189))),
push_actions: Default::default(),
thread_summary: ThreadSummaryStatus::Some(ThreadSummary {
num_replies: 2,
latest_reply: None,
}),
bundled_latest_thread_event: None,
};
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
assert_json_snapshot! {
serde_json::to_value(&room_event).unwrap(),
}
});
}
}