use crate::ids::{DeviceId, EventId, RoomId, UserId};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RawEvent {
pub event_id: EventId,
#[serde(rename = "type")]
pub event_type: String,
pub sender: UserId,
pub origin_server_ts: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_key: Option<String>,
#[serde(default)]
pub content: serde_json::Value,
#[serde(default)]
pub unsigned: Unsigned,
}
impl RawEvent {
pub fn relates_to(&self) -> Option<RelatesTo> {
RelatesTo::from_content(&self.content)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Unsigned {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub age: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transaction_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redacted_because: Option<Box<RawEvent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prev_content: Option<serde_json::Value>,
#[serde(rename = "m.relations", default, skip_serializing_if = "Option::is_none")]
pub relations: Option<serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StrippedStateEvent {
#[serde(rename = "type")]
pub event_type: String,
pub state_key: String,
pub sender: UserId,
#[serde(default)]
pub content: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BasicEvent {
#[serde(rename = "type")]
pub event_type: String,
#[serde(default)]
pub content: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToDeviceEvent {
pub sender: UserId,
#[serde(rename = "type")]
pub event_type: String,
#[serde(default)]
pub content: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RelatesTo {
Annotation {
event_id: EventId,
key: String,
},
Replace {
event_id: EventId,
},
Thread {
event_id: EventId,
is_falling_back: bool,
in_reply_to: Option<InReplyTo>,
},
InReplyTo(InReplyTo),
Unknown(serde_json::Value),
}
impl RelatesTo {
pub fn from_content(content: &serde_json::Value) -> Option<Self> {
content.get("m.relates_to").map(RelatesTo::from_value)
}
fn from_value(value: &serde_json::Value) -> Self {
let rel_type = value.get("rel_type").and_then(serde_json::Value::as_str);
match rel_type {
Some("m.annotation") => {
let event_id = value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok());
let key = value.get("key").and_then(serde_json::Value::as_str);
if let (Some(event_id), Some(key)) = (event_id, key) {
return RelatesTo::Annotation { event_id, key: key.to_string() };
}
}
Some("m.replace") => {
if let Some(event_id) =
value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok())
{
return RelatesTo::Replace { event_id };
}
}
Some("m.thread") => {
if let Some(event_id) =
value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok())
{
let is_falling_back =
value.get("is_falling_back").and_then(serde_json::Value::as_bool).unwrap_or(false);
let in_reply_to = value.get("m.in_reply_to").and_then(InReplyTo::from_value);
return RelatesTo::Thread { event_id, is_falling_back, in_reply_to };
}
}
None => {
if let Some(in_reply_to) = value.get("m.in_reply_to").and_then(InReplyTo::from_value) {
return RelatesTo::InReplyTo(in_reply_to);
}
}
Some(_) => {}
}
RelatesTo::Unknown(value.clone())
}
}
impl Serialize for RelatesTo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let value = match self {
RelatesTo::Annotation { event_id, key } => {
serde_json::json!({ "rel_type": "m.annotation", "event_id": event_id.as_str(), "key": key })
}
RelatesTo::Replace { event_id } => {
serde_json::json!({ "rel_type": "m.replace", "event_id": event_id.as_str() })
}
RelatesTo::Thread { event_id, is_falling_back, in_reply_to } => {
let mut value = serde_json::json!({
"rel_type": "m.thread",
"event_id": event_id.as_str(),
"is_falling_back": is_falling_back,
});
if let Some(in_reply_to) = in_reply_to {
value["m.in_reply_to"] = serde_json::json!({ "event_id": in_reply_to.event_id.as_str() });
}
value
}
RelatesTo::InReplyTo(reply) => {
serde_json::json!({ "m.in_reply_to": { "event_id": reply.event_id.as_str() } })
}
RelatesTo::Unknown(value) => value.clone(),
};
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for RelatesTo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
Ok(RelatesTo::from_value(&value))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InReplyTo {
pub event_id: EventId,
}
impl InReplyTo {
fn from_value(value: &serde_json::Value) -> Option<Self> {
let event_id = value.get("event_id")?.as_str()?;
EventId::parse(event_id).ok().map(|event_id| InReplyTo { event_id })
}
}
fn serialize_tagged<S, T>(serializer: S, tag_field: &str, tag_value: &str, inner: &T) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: Serialize,
{
let mut value = serde_json::to_value(inner).map_err(serde::ser::Error::custom)?;
if let serde_json::Value::Object(map) = &mut value {
map.insert(tag_field.to_string(), serde_json::Value::String(tag_value.to_string()));
}
value.serialize(serializer)
}
fn default_room_version() -> String {
"11".to_string()
}
fn default_true() -> bool {
true
}
fn default_power_level_50() -> i64 {
50
}
fn default_rotation_period_ms() -> i64 {
604_800_000
}
fn default_rotation_period_msgs() -> i64 {
100
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomCreateContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creator: Option<UserId>,
#[serde(default = "default_room_version")]
pub room_version: String,
#[serde(rename = "m.federate", default = "default_true")]
pub federate: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predecessor: Option<RoomCreatePredecessor>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomCreatePredecessor {
pub room_id: RoomId,
pub event_id: EventId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Membership {
Invite,
Join,
Knock,
Leave,
Ban,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomMemberContent {
pub membership: Membership,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_direct: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomPowerLevelsNotifications {
#[serde(default = "default_power_level_50")]
pub room: i64,
}
impl Default for RoomPowerLevelsNotifications {
fn default() -> Self {
Self { room: 50 }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomPowerLevelsContent {
#[serde(default = "default_power_level_50")]
pub ban: i64,
#[serde(default)]
pub events: BTreeMap<String, i64>,
#[serde(default)]
pub events_default: i64,
#[serde(default = "default_power_level_50")]
pub invite: i64,
#[serde(default = "default_power_level_50")]
pub kick: i64,
#[serde(default = "default_power_level_50")]
pub redact: i64,
#[serde(default = "default_power_level_50")]
pub state_default: i64,
#[serde(default)]
pub users: BTreeMap<UserId, i64>,
#[serde(default)]
pub users_default: i64,
#[serde(default)]
pub notifications: RoomPowerLevelsNotifications,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JoinRule {
Public,
Invite,
Knock,
Private,
Restricted,
KnockRestricted,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinRuleAllow {
#[serde(rename = "type")]
pub allow_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub room_id: Option<RoomId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomJoinRulesContent {
pub join_rule: JoinRule,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allow: Vec<JoinRuleAllow>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HistoryVisibility {
Invited,
Joined,
Shared,
WorldReadable,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomHistoryVisibilityContent {
pub history_visibility: HistoryVisibility,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomNameContent {
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomTopicContent {
pub topic: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomEncryptionContent {
pub algorithm: String,
#[serde(default = "default_rotation_period_ms")]
pub rotation_period_ms: i64,
#[serde(default = "default_rotation_period_msgs")]
pub rotation_period_msgs: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomPinnedEventsContent {
pub pinned: Vec<EventId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextLikeMessageContent {
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub formatted_body: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RoomMessageContent {
Text(TextLikeMessageContent),
Notice(TextLikeMessageContent),
Emote(TextLikeMessageContent),
Unknown(serde_json::Value),
}
impl Serialize for RoomMessageContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
RoomMessageContent::Text(inner) => serialize_tagged(serializer, "msgtype", "m.text", inner),
RoomMessageContent::Notice(inner) => serialize_tagged(serializer, "msgtype", "m.notice", inner),
RoomMessageContent::Emote(inner) => serialize_tagged(serializer, "msgtype", "m.emote", inner),
RoomMessageContent::Unknown(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RoomMessageContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let msgtype = value.get("msgtype").and_then(serde_json::Value::as_str).map(str::to_owned);
match msgtype.as_deref() {
Some("m.text") => serde_json::from_value(value).map(RoomMessageContent::Text).map_err(serde::de::Error::custom),
Some("m.notice") => {
serde_json::from_value(value).map(RoomMessageContent::Notice).map_err(serde::de::Error::custom)
}
Some("m.emote") => {
serde_json::from_value(value).map(RoomMessageContent::Emote).map_err(serde::de::Error::custom)
}
_ => Ok(RoomMessageContent::Unknown(value)),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MegolmEncryptedContent {
pub ciphertext: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub device_id: Option<DeviceId>,
#[serde(rename = "m.relates_to", default, skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelatesTo>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OlmCiphertextInfo {
#[serde(rename = "type")]
pub message_type: u8,
pub body: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OlmEncryptedContent {
pub sender_key: String,
pub ciphertext: BTreeMap<String, OlmCiphertextInfo>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RoomEncryptedContent {
Megolm(MegolmEncryptedContent),
Olm(OlmEncryptedContent),
Unknown(serde_json::Value),
}
impl Serialize for RoomEncryptedContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
RoomEncryptedContent::Megolm(inner) => {
serialize_tagged(serializer, "algorithm", "m.megolm.v1.aes-sha2", inner)
}
RoomEncryptedContent::Olm(inner) => {
serialize_tagged(serializer, "algorithm", "m.olm.v1.curve25519-aes-sha2", inner)
}
RoomEncryptedContent::Unknown(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RoomEncryptedContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let algorithm = value.get("algorithm").and_then(serde_json::Value::as_str).map(str::to_owned);
match algorithm.as_deref() {
Some("m.megolm.v1.aes-sha2") => {
serde_json::from_value(value).map(RoomEncryptedContent::Megolm).map_err(serde::de::Error::custom)
}
Some("m.olm.v1.curve25519-aes-sha2") => {
serde_json::from_value(value).map(RoomEncryptedContent::Olm).map_err(serde::de::Error::custom)
}
_ => Ok(RoomEncryptedContent::Unknown(value)),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ReactionContent {
#[serde(rename = "m.relates_to")]
pub relates_to: RelatesTo,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RedactionContent {
pub redacts: EventId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomKeyContent {
pub algorithm: String,
pub room_id: RoomId,
pub session_id: String,
pub session_key: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForwardedRoomKeyContent {
pub algorithm: String,
pub room_id: RoomId,
pub session_id: String,
pub session_key: String,
pub sender_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_claimed_ed25519_key: Option<String>,
#[serde(default)]
pub forwarding_curve25519_key_chain: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoomKeyRequestAction {
Request,
RequestCancellation,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomKeyRequestBody {
pub algorithm: String,
pub room_id: RoomId,
pub session_id: String,
pub sender_key: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomKeyRequestContent {
pub action: RoomKeyRequestAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<RoomKeyRequestBody>,
pub request_id: String,
pub requesting_device_id: DeviceId,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WithheldCode {
Blacklisted,
Unverified,
Unauthorised,
Unavailable,
NoOlm,
Unknown(String),
}
impl WithheldCode {
pub fn as_str(&self) -> &str {
match self {
WithheldCode::Blacklisted => "m.blacklisted",
WithheldCode::Unverified => "m.unverified",
WithheldCode::Unauthorised => "m.unauthorised",
WithheldCode::Unavailable => "m.unavailable",
WithheldCode::NoOlm => "m.no_olm",
WithheldCode::Unknown(raw) => raw.as_str(),
}
}
}
impl Serialize for WithheldCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for WithheldCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Ok(match raw.as_str() {
"m.blacklisted" => WithheldCode::Blacklisted,
"m.unverified" => WithheldCode::Unverified,
"m.unauthorised" => WithheldCode::Unauthorised,
"m.unavailable" => WithheldCode::Unavailable,
"m.no_olm" => WithheldCode::NoOlm,
_ => WithheldCode::Unknown(raw),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomKeyWithheldContent {
pub algorithm: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub room_id: Option<RoomId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub sender_key: String,
pub code: WithheldCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DummyContent {}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DirectContent(pub BTreeMap<UserId, Vec<RoomId>>);
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TagInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TagContent {
#[serde(default)]
pub tags: BTreeMap<String, TagInfo>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FullyReadContent {
pub event_id: EventId,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypingContent {
#[serde(default)]
pub user_ids: Vec<UserId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReceiptEntry {
pub ts: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ReceiptContent(pub BTreeMap<EventId, BTreeMap<String, BTreeMap<UserId, ReceiptEntry>>>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn room_create_content_decodes_without_a_creator_field() {
let v11: RoomCreateContent = serde_json::from_value(serde_json::json!({ "room_version": "11" })).expect("v11 create content decodes");
assert_eq!(v11.creator, None);
assert_eq!(v11.room_version, "11");
let v10: RoomCreateContent =
serde_json::from_value(serde_json::json!({ "creator": "@a:example.org", "room_version": "10" })).expect("v10 create content decodes");
assert_eq!(v10.creator.as_ref().map(UserId::as_str), Some("@a:example.org"));
}
#[test]
fn parses_relates_to_annotation_replace_thread() {
let annotation = serde_json::json!({
"m.relates_to": { "rel_type": "m.annotation", "event_id": "$target:example.org", "key": "👍" }
});
match RelatesTo::from_content(&annotation) {
Some(RelatesTo::Annotation { event_id, key }) => {
assert_eq!(event_id.as_str(), "$target:example.org");
assert_eq!(key, "👍");
}
other => panic!("expected Annotation, got {other:?}"),
}
let replace = serde_json::json!({
"m.relates_to": { "rel_type": "m.replace", "event_id": "$original:example.org" },
"m.new_content": { "msgtype": "m.text", "body": "edited" }
});
match RelatesTo::from_content(&replace) {
Some(RelatesTo::Replace { event_id }) => assert_eq!(event_id.as_str(), "$original:example.org"),
other => panic!("expected Replace, got {other:?}"),
}
let thread = serde_json::json!({
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$root:example.org",
"is_falling_back": true,
"m.in_reply_to": { "event_id": "$latest:example.org" }
}
});
match RelatesTo::from_content(&thread) {
Some(RelatesTo::Thread { event_id, is_falling_back, in_reply_to }) => {
assert_eq!(event_id.as_str(), "$root:example.org");
assert!(is_falling_back);
assert_eq!(in_reply_to.expect("in_reply_to present").event_id.as_str(), "$latest:example.org");
}
other => panic!("expected Thread, got {other:?}"),
}
let legacy_reply = serde_json::json!({
"m.relates_to": { "m.in_reply_to": { "event_id": "$legacy:example.org" } }
});
match RelatesTo::from_content(&legacy_reply) {
Some(RelatesTo::InReplyTo(reply)) => assert_eq!(reply.event_id.as_str(), "$legacy:example.org"),
other => panic!("expected InReplyTo, got {other:?}"),
}
let no_relation = serde_json::json!({ "msgtype": "m.text", "body": "plain" });
assert_eq!(RelatesTo::from_content(&no_relation), None);
}
#[test]
fn unrecognized_relation_shape_becomes_unknown_not_a_parse_failure() {
let content = serde_json::json!({ "m.relates_to": { "rel_type": "org.example.future_relation", "foo": "bar" } });
match RelatesTo::from_content(&content) {
Some(RelatesTo::Unknown(value)) => {
assert_eq!(value["rel_type"], "org.example.future_relation");
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn unknown_event_type_is_kept_raw() {
let json = serde_json::json!({
"event_id": "$abc:example.org",
"type": "org.example.made_up_event",
"sender": "@alice:example.org",
"origin_server_ts": 1_700_000_000_000i64,
"content": { "whatever": ["this", "is"], "n": 3 },
"unsigned": { "age": 12 }
});
let event: RawEvent = serde_json::from_value(json).expect("unrecognized type still parses");
assert_eq!(event.event_type, "org.example.made_up_event");
assert_eq!(event.content["whatever"][0], "this");
assert_eq!(event.content["n"], 3);
assert_eq!(event.unsigned.age, Some(12));
assert_eq!(event.state_key, None);
}
#[test]
fn redacted_because_nests_the_redaction_event() {
let json = serde_json::json!({
"event_id": "$abc:example.org",
"type": "m.room.message",
"sender": "@alice:example.org",
"origin_server_ts": 1,
"content": {},
"unsigned": {
"redacted_because": {
"event_id": "$redaction:example.org",
"type": "m.room.redaction",
"sender": "@mod:example.org",
"origin_server_ts": 2,
"content": { "redacts": "$abc:example.org" }
}
}
});
let event: RawEvent = serde_json::from_value(json).expect("valid event");
let redaction = event.unsigned.redacted_because.expect("redacted_because present");
assert_eq!(redaction.event_type, "m.room.redaction");
let redaction_content: RedactionContent =
serde_json::from_value(redaction.content.clone()).expect("valid redaction content");
assert_eq!(redaction_content.redacts.as_str(), "$abc:example.org");
}
#[test]
fn encrypted_content_parses_megolm_and_olm_shapes() {
let megolm = serde_json::json!({
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "cipher-bytes-b64",
"session_id": "session-1",
"sender_key": "sender-curve25519",
"device_id": "DEVICE1"
});
match serde_json::from_value::<RoomEncryptedContent>(megolm).expect("valid megolm content") {
RoomEncryptedContent::Megolm(inner) => {
assert_eq!(inner.ciphertext, "cipher-bytes-b64");
assert_eq!(inner.session_id, "session-1");
assert_eq!(inner.sender_key.as_deref(), Some("sender-curve25519"));
assert_eq!(inner.device_id.map(|d| d.as_str().to_string()), Some("DEVICE1".to_string()));
}
other => panic!("expected Megolm, got {other:?}"),
}
let olm = serde_json::json!({
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"sender_key": "sender-curve25519",
"ciphertext": {
"recipient-curve25519": { "type": 0, "body": "prekey-message-b64" }
}
});
match serde_json::from_value::<RoomEncryptedContent>(olm).expect("valid olm content") {
RoomEncryptedContent::Olm(inner) => {
assert_eq!(inner.sender_key, "sender-curve25519");
let entry = inner.ciphertext.get("recipient-curve25519").expect("recipient entry present");
assert_eq!(entry.message_type, 0);
assert_eq!(entry.body, "prekey-message-b64");
}
other => panic!("expected Olm, got {other:?}"),
}
let unknown = serde_json::json!({ "algorithm": "org.example.future-algorithm", "opaque": true });
match serde_json::from_value::<RoomEncryptedContent>(unknown).expect("unrecognized algorithm still parses") {
RoomEncryptedContent::Unknown(value) => assert_eq!(value["opaque"], true),
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn room_message_unknown_msgtype_is_kept_raw() {
let image = serde_json::json!({ "msgtype": "m.image", "body": "cat.png", "url": "mxc://example.org/abc" });
match serde_json::from_value::<RoomMessageContent>(image).expect("unrecognized msgtype still parses") {
RoomMessageContent::Unknown(value) => assert_eq!(value["url"], "mxc://example.org/abc"),
other => panic!("expected Unknown, got {other:?}"),
}
let text = serde_json::json!({ "msgtype": "m.text", "body": "hi", "formatted_body": "<b>hi</b>", "format": "org.matrix.custom.html" });
match serde_json::from_value::<RoomMessageContent>(text).expect("valid text content") {
RoomMessageContent::Text(inner) => {
assert_eq!(inner.body, "hi");
assert_eq!(inner.formatted_body.as_deref(), Some("<b>hi</b>"));
}
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
fn power_levels_defaults_apply() {
let content: RoomPowerLevelsContent = serde_json::from_value(serde_json::json!({})).expect("empty object parses");
assert_eq!(content.ban, 50);
assert_eq!(content.invite, 50);
assert_eq!(content.kick, 50);
assert_eq!(content.redact, 50);
assert_eq!(content.state_default, 50);
assert_eq!(content.events_default, 0);
assert_eq!(content.users_default, 0);
assert_eq!(content.notifications.room, 50);
assert!(content.events.is_empty());
assert!(content.users.is_empty());
}
#[test]
fn power_levels_overrides_the_spec_defaults() {
let content: RoomPowerLevelsContent = serde_json::from_value(serde_json::json!({
"ban": 100,
"users": { "@admin:example.org": 100 },
"events": { "m.room.name": 50 }
}))
.expect("valid power levels");
assert_eq!(content.ban, 100);
let admin = UserId::parse("@admin:example.org").expect("valid user id");
assert_eq!(content.users.get(&admin), Some(&100));
assert_eq!(content.events.get("m.room.name"), Some(&50));
assert_eq!(content.invite, 50);
}
#[test]
fn receipt_content_parses_nested_map() {
let json = serde_json::json!({
"$event1:example.org": {
"m.read": {
"@alice:example.org": { "ts": 1_436_451_550_453i64, "thread_id": "main" }
},
"m.read.private": {
"@alice:example.org": { "ts": 1_436_451_551_000i64 }
}
}
});
let receipts: ReceiptContent = serde_json::from_value(json).expect("valid receipt content");
let event_id = EventId::parse("$event1:example.org").expect("valid event id");
let alice = UserId::parse("@alice:example.org").expect("valid user id");
let by_type = receipts.0.get(&event_id).expect("event present");
let read = by_type.get("m.read").expect("m.read present").get(&alice).expect("alice present");
assert_eq!(read.ts, 1_436_451_550_453);
assert_eq!(read.thread_id.as_deref(), Some("main"));
let read_private =
by_type.get("m.read.private").expect("m.read.private present").get(&alice).expect("alice present");
assert_eq!(read_private.ts, 1_436_451_551_000);
assert_eq!(read_private.thread_id, None);
}
#[test]
fn withheld_codes_parse_including_unknown() {
let cases = [
("m.blacklisted", WithheldCode::Blacklisted),
("m.unverified", WithheldCode::Unverified),
("m.unauthorised", WithheldCode::Unauthorised),
("m.unavailable", WithheldCode::Unavailable),
("m.no_olm", WithheldCode::NoOlm),
];
for (wire, expected) in cases {
let parsed: WithheldCode = serde_json::from_value(serde_json::json!(wire)).expect("known code parses");
assert_eq!(parsed, expected);
assert_eq!(parsed.as_str(), wire);
}
let unknown: WithheldCode =
serde_json::from_value(serde_json::json!("m.some_future_code")).expect("unknown code still parses");
assert_eq!(unknown, WithheldCode::Unknown("m.some_future_code".to_string()));
assert_eq!(unknown.as_str(), "m.some_future_code");
let content: RoomKeyWithheldContent = serde_json::from_value(serde_json::json!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:example.org",
"session_id": "session-1",
"sender_key": "sender-curve25519",
"code": "m.unverified",
"reason": "device not verified"
}))
.expect("valid withheld content");
assert_eq!(content.code, WithheldCode::Unverified);
assert_eq!(content.reason.as_deref(), Some("device not verified"));
}
}