Skip to main content

mail4agent_messenger/wire/
events.rs

1//! Matrix event envelope, relation aggregation, and typed event `content`
2//! shapes — research doc §1.1 (envelope), §1.2 (state events), §1.3
3//! (timeline events + relations), §3.1-§3.3 (E2EE to-device payloads),
4//! §1.6-§1.7 (ephemeral + account data), and the server plan's manager
5//! decision #1 (spec v1.19, room version 11). An event type this module
6//! does not model stays a raw [`serde_json::Value`] on [`RawEvent`].
7//!
8//! # Never fails on an unrecognized event
9//!
10//! [`RawEvent::content`] is always a plain [`serde_json::Value`] — parsing a
11//! `RawEvent` off the wire never depends on recognizing its `type`. Every
12//! typed content struct in this module (`RoomMessageContent`,
13//! `RoomEncryptedContent`, `WithheldCode`, ...) is something a caller
14//! deserializes from that `content` value *on demand*, once it already knows
15//! which type it is dealing with; an unrecognized `msgtype`/`algorithm`/
16//! `code` inside one of those typed shapes falls back to an `Unknown`
17//! variant carrying the raw value rather than failing to deserialize — same
18//! "forward compatible by default" doctrine `crate::store`'s record-key
19//! parser already follows.
20
21use crate::ids::{DeviceId, EventId, RoomId, UserId};
22use serde::{Deserialize, Serialize};
23use std::collections::BTreeMap;
24
25// ---------------------------------------------------------------------
26// Event envelope (research §1.1)
27// ---------------------------------------------------------------------
28
29/// One state or timeline event exactly as the client-server API represents
30/// it (research doc §1.1's envelope). `state_key` present means this is a
31/// state event; absent means a timeline/message event.
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub struct RawEvent {
34    /// The event's own id.
35    pub event_id: EventId,
36    /// The event type (`"m.room.message"`, `"m.room.member"`, ...). Never
37    /// validated against a known set — see the module doc.
38    #[serde(rename = "type")]
39    pub event_type: String,
40    /// The event's sender.
41    pub sender: UserId,
42    /// Milliseconds since the Unix epoch, server-assigned.
43    pub origin_server_ts: i64,
44    /// Present for a state event (the state's key, often empty or a user
45    /// id); absent for a timeline/message event.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub state_key: Option<String>,
48    /// The event's type-specific payload, kept untyped at this layer.
49    #[serde(default)]
50    pub content: serde_json::Value,
51    /// Server-attached metadata not part of the event's own signed content.
52    #[serde(default)]
53    pub unsigned: Unsigned,
54}
55
56impl RawEvent {
57    /// Parses this event's `content["m.relates_to"]` field, if present. See
58    /// [`RelatesTo::from_content`] for what happens when the field is
59    /// present but doesn't match a shape this crate recognizes.
60    pub fn relates_to(&self) -> Option<RelatesTo> {
61        RelatesTo::from_content(&self.content)
62    }
63}
64
65/// `unsigned`, research doc §1.1: metadata the server attaches to an event
66/// that is not part of what the event's own signature covers.
67#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
68pub struct Unsigned {
69    /// Milliseconds since `origin_server_ts` that have elapsed on the
70    /// server, as of the response that carried this event.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub age: Option<i64>,
73    /// Echoed back to the sender's own client: the `txnId` it used to send
74    /// this event, letting a local echo reconcile against the confirmed
75    /// event (a later piece, `room::timeline`).
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub transaction_id: Option<String>,
78    /// Present once this event has been redacted: the redaction event
79    /// itself. Boxed because a redaction event can, recursively, carry its
80    /// own (empty) `unsigned`.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub redacted_because: Option<Box<RawEvent>>,
83    /// For a state event, the state's content immediately before this
84    /// event, if the requesting client asked for it.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub prev_content: Option<serde_json::Value>,
87    /// The `m.relations` bundle (aggregated reactions, the latest edit,
88    /// thread summary, ...) the server attaches to an event other events
89    /// relate to. Kept as a raw value: aggregation shape varies by relation
90    /// kind and this crate does not need to interpret the bundle itself to
91    /// parse the envelope (`room::relations`, a later piece, is where this
92    /// gets used).
93    #[serde(rename = "m.relations", default, skip_serializing_if = "Option::is_none")]
94    pub relations: Option<serde_json::Value>,
95}
96
97/// A stripped-down state event as sent in an invite's `invite_state`
98/// preview (research doc §1.4) — the same fields an `m.room.member`/etc.
99/// state event carries, minus everything that would require the recipient
100/// to already have joined the room to make sense of (`event_id`,
101/// `origin_server_ts`, `unsigned`).
102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
103pub struct StrippedStateEvent {
104    /// The state event's type.
105    #[serde(rename = "type")]
106    pub event_type: String,
107    /// The state event's key.
108    pub state_key: String,
109    /// The state event's sender.
110    pub sender: UserId,
111    /// The state event's content, untyped at this layer (see the module
112    /// doc).
113    #[serde(default)]
114    pub content: serde_json::Value,
115}
116
117/// The minimal `{type, content}` shape shared by ephemeral (`m.typing`,
118/// `m.receipt`) and account-data (`m.tag`, `m.direct`, `m.fully_read`, ...)
119/// events (research doc §1.6/§1.7) — unlike [`RawEvent`], these carry no
120/// `event_id`, `sender`, or `origin_server_ts` on the wire.
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
122pub struct BasicEvent {
123    /// The event's type.
124    #[serde(rename = "type")]
125    pub event_type: String,
126    /// The event's content, untyped at this layer (see the module doc).
127    #[serde(default)]
128    pub content: serde_json::Value,
129}
130
131/// A to-device event (research doc §3.3): delivered out-of-band from any
132/// room's timeline, one per `(user, device)`. Carries no `event_id` or
133/// `origin_server_ts` on the wire.
134#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
135pub struct ToDeviceEvent {
136    /// The sending user.
137    pub sender: UserId,
138    /// The event's type (`"m.room_key"`, `"m.room.encrypted"`, ...).
139    #[serde(rename = "type")]
140    pub event_type: String,
141    /// The event's content, untyped at this layer (see the module doc).
142    #[serde(default)]
143    pub content: serde_json::Value,
144}
145
146// ---------------------------------------------------------------------
147// Relations (research §1.3)
148// ---------------------------------------------------------------------
149
150/// One event's `m.relates_to` field, parsed into the shape its `rel_type`
151/// (or, for the legacy reply pointer, the absence of one) implies. Never
152/// fails to parse: an `m.relates_to` value that matches none of the shapes
153/// below becomes [`RelatesTo::Unknown`], carrying the raw value.
154#[derive(Clone, Debug, PartialEq)]
155pub enum RelatesTo {
156    /// `m.annotation` (reactions): `key` is the reaction's own content
157    /// (e.g. an emoji), `event_id` the event being reacted to.
158    Annotation {
159        /// The event being annotated.
160        event_id: EventId,
161        /// The annotation's key (an emoji, for `m.reaction`).
162        key: String,
163    },
164    /// `m.replace` (edits, MSC2676): `event_id` is the event being
165    /// replaced. The replacement content itself lives at the top level of
166    /// the *edit* event's own content, under `m.new_content` — not part of
167    /// this type, since that key sits alongside `m.relates_to`, not inside
168    /// it.
169    Replace {
170        /// The event being replaced.
171        event_id: EventId,
172    },
173    /// `m.thread` (MSC3440).
174    Thread {
175        /// The thread's root event.
176        event_id: EventId,
177        /// `true` if this reply also sets `m.in_reply_to` purely for
178        /// clients without threading UI, and should not be shown twice by
179        /// a client that does understand threads.
180        is_falling_back: bool,
181        /// The specific event within the thread this one is a rich reply
182        /// to, if any.
183        in_reply_to: Option<InReplyTo>,
184    },
185    /// The legacy rich-reply pointer (`m.in_reply_to` with no `rel_type`
186    /// alongside it) — still sent for backward compatibility even when an
187    /// event carries a "real" relation.
188    InReplyTo(InReplyTo),
189    /// An `m.relates_to` value present but not matching any shape above —
190    /// kept verbatim rather than rejected (module doc).
191    Unknown(serde_json::Value),
192}
193
194impl RelatesTo {
195    /// Extracts and parses `content["m.relates_to"]`, if present. Returns
196    /// `None` only when the field itself is absent — most events don't
197    /// relate to anything. See this type's own doc for what happens when
198    /// the field is present but unrecognized.
199    pub fn from_content(content: &serde_json::Value) -> Option<Self> {
200        content.get("m.relates_to").map(RelatesTo::from_value)
201    }
202
203    fn from_value(value: &serde_json::Value) -> Self {
204        let rel_type = value.get("rel_type").and_then(serde_json::Value::as_str);
205        match rel_type {
206            Some("m.annotation") => {
207                let event_id = value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok());
208                let key = value.get("key").and_then(serde_json::Value::as_str);
209                if let (Some(event_id), Some(key)) = (event_id, key) {
210                    return RelatesTo::Annotation { event_id, key: key.to_string() };
211                }
212            }
213            Some("m.replace") => {
214                if let Some(event_id) =
215                    value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok())
216                {
217                    return RelatesTo::Replace { event_id };
218                }
219            }
220            Some("m.thread") => {
221                if let Some(event_id) =
222                    value.get("event_id").and_then(serde_json::Value::as_str).and_then(|s| EventId::parse(s).ok())
223                {
224                    let is_falling_back =
225                        value.get("is_falling_back").and_then(serde_json::Value::as_bool).unwrap_or(false);
226                    let in_reply_to = value.get("m.in_reply_to").and_then(InReplyTo::from_value);
227                    return RelatesTo::Thread { event_id, is_falling_back, in_reply_to };
228                }
229            }
230            None => {
231                if let Some(in_reply_to) = value.get("m.in_reply_to").and_then(InReplyTo::from_value) {
232                    return RelatesTo::InReplyTo(in_reply_to);
233                }
234            }
235            Some(_) => {}
236        }
237        RelatesTo::Unknown(value.clone())
238    }
239}
240
241impl Serialize for RelatesTo {
242    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243    where
244        S: serde::Serializer,
245    {
246        let value = match self {
247            RelatesTo::Annotation { event_id, key } => {
248                serde_json::json!({ "rel_type": "m.annotation", "event_id": event_id.as_str(), "key": key })
249            }
250            RelatesTo::Replace { event_id } => {
251                serde_json::json!({ "rel_type": "m.replace", "event_id": event_id.as_str() })
252            }
253            RelatesTo::Thread { event_id, is_falling_back, in_reply_to } => {
254                let mut value = serde_json::json!({
255                    "rel_type": "m.thread",
256                    "event_id": event_id.as_str(),
257                    "is_falling_back": is_falling_back,
258                });
259                if let Some(in_reply_to) = in_reply_to {
260                    value["m.in_reply_to"] = serde_json::json!({ "event_id": in_reply_to.event_id.as_str() });
261                }
262                value
263            }
264            RelatesTo::InReplyTo(reply) => {
265                serde_json::json!({ "m.in_reply_to": { "event_id": reply.event_id.as_str() } })
266            }
267            RelatesTo::Unknown(value) => value.clone(),
268        };
269        value.serialize(serializer)
270    }
271}
272
273impl<'de> Deserialize<'de> for RelatesTo {
274    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
275    where
276        D: serde::Deserializer<'de>,
277    {
278        let value = serde_json::Value::deserialize(deserializer)?;
279        Ok(RelatesTo::from_value(&value))
280    }
281}
282
283/// The `m.in_reply_to` pointer, either standalone (legacy reply) or nested
284/// inside an [`RelatesTo::Thread`].
285#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
286pub struct InReplyTo {
287    /// The event being replied to.
288    pub event_id: EventId,
289}
290
291impl InReplyTo {
292    fn from_value(value: &serde_json::Value) -> Option<Self> {
293        let event_id = value.get("event_id")?.as_str()?;
294        EventId::parse(event_id).ok().map(|event_id| InReplyTo { event_id })
295    }
296}
297
298/// Reserializes `inner` and injects `tag_field: tag_value` into the result
299/// — the shared shape [`RoomMessageContent`] and [`RoomEncryptedContent`]
300/// both serialize through, so a discriminant field derived structs don't
301/// carry (`msgtype`, `algorithm`) round-trips correctly.
302fn serialize_tagged<S, T>(serializer: S, tag_field: &str, tag_value: &str, inner: &T) -> Result<S::Ok, S::Error>
303where
304    S: serde::Serializer,
305    T: Serialize,
306{
307    let mut value = serde_json::to_value(inner).map_err(serde::ser::Error::custom)?;
308    if let serde_json::Value::Object(map) = &mut value {
309        map.insert(tag_field.to_string(), serde_json::Value::String(tag_value.to_string()));
310    }
311    value.serialize(serializer)
312}
313
314// ---------------------------------------------------------------------
315// State event content (research §1.2, server plan §1 room version 11)
316// ---------------------------------------------------------------------
317
318fn default_room_version() -> String {
319    "11".to_string()
320}
321
322fn default_true() -> bool {
323    true
324}
325
326fn default_power_level_50() -> i64 {
327    50
328}
329
330fn default_rotation_period_ms() -> i64 {
331    604_800_000
332}
333
334fn default_rotation_period_msgs() -> i64 {
335    100
336}
337
338/// `m.room.create` content. Room version 11 dropped the `creator` field (the
339/// creator is the event's own sender), so `creator` is optional: a required
340/// field would make every `/sync` that carried a version-11 create event
341/// fail to decode.
342#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
343pub struct RoomCreateContent {
344    /// The room's creator, only present on pre-v11 rooms.
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub creator: Option<UserId>,
347    /// The room version this room was created with.
348    #[serde(default = "default_room_version")]
349    pub room_version: String,
350    /// Whether users on other servers may join (`m.federate`) — meaningless
351    /// for a single, non-federated server (research doc §1.2) but kept for
352    /// wire-shape fidelity.
353    #[serde(rename = "m.federate", default = "default_true")]
354    pub federate: bool,
355    /// The room this one replaced via a room upgrade, if any.
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub predecessor: Option<RoomCreatePredecessor>,
358}
359
360/// `m.room.create`'s `predecessor` field.
361#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
362pub struct RoomCreatePredecessor {
363    /// The predecessor room's id.
364    pub room_id: RoomId,
365    /// The last known event id in the predecessor room.
366    pub event_id: EventId,
367}
368
369/// A room membership value (`m.room.member`'s `membership` field).
370#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "lowercase")]
372pub enum Membership {
373    /// Invited, not yet joined.
374    Invite,
375    /// Currently a member.
376    Join,
377    /// Requested to join a room with a restrictive join rule.
378    Knock,
379    /// Left (or never joined and the invite/knock was withdrawn).
380    Leave,
381    /// Banned.
382    Ban,
383    /// A membership value this crate does not recognize yet — kept rather
384    /// than failing the surrounding event's parse (module doc).
385    #[serde(other)]
386    Unknown,
387}
388
389/// `m.room.member` content, keyed by `state_key = user_id`.
390#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
391pub struct RoomMemberContent {
392    /// The subject's membership state.
393    pub membership: Membership,
394    /// The subject's display name, if set.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub displayname: Option<String>,
397    /// `true` when this invite/join is part of a direct-message room.
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub is_direct: Option<bool>,
400}
401
402/// `m.room.power_levels`'s `notifications` sub-object. Defaults to `{room:
403/// 50}` per spec, not to zero — hence a hand-written [`Default`] rather than
404/// a derived one.
405#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
406pub struct RoomPowerLevelsNotifications {
407    /// The power level required to trigger an `@room` notification.
408    #[serde(default = "default_power_level_50")]
409    pub room: i64,
410}
411
412impl Default for RoomPowerLevelsNotifications {
413    fn default() -> Self {
414        Self { room: 50 }
415    }
416}
417
418/// `m.room.power_levels` content, every field defaulted per spec (research
419/// doc §1.2) so a sparse power-levels event (including `{}`) parses to the
420/// spec's own baseline permission table.
421#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
422pub struct RoomPowerLevelsContent {
423    /// Power level required to ban a user. Default `50`.
424    #[serde(default = "default_power_level_50")]
425    pub ban: i64,
426    /// Per-event-type power level overrides (event type → required level).
427    #[serde(default)]
428    pub events: BTreeMap<String, i64>,
429    /// Power level required to send an event whose type has no entry in
430    /// `events`. Default `0`.
431    #[serde(default)]
432    pub events_default: i64,
433    /// Power level required to invite a user. Default `50`.
434    #[serde(default = "default_power_level_50")]
435    pub invite: i64,
436    /// Power level required to kick a user. Default `50`.
437    #[serde(default = "default_power_level_50")]
438    pub kick: i64,
439    /// Power level required to redact an event. Default `50`.
440    #[serde(default = "default_power_level_50")]
441    pub redact: i64,
442    /// Power level required to send a state event whose type has no entry
443    /// in `events`. Default `50`.
444    #[serde(default = "default_power_level_50")]
445    pub state_default: i64,
446    /// Per-user power level overrides (user id → level).
447    #[serde(default)]
448    pub users: BTreeMap<UserId, i64>,
449    /// Power level a user has when absent from `users`. Default `0`.
450    #[serde(default)]
451    pub users_default: i64,
452    /// Power levels required to trigger a notification.
453    #[serde(default)]
454    pub notifications: RoomPowerLevelsNotifications,
455}
456
457/// `m.room.join_rules`'s `join_rule` value.
458#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
459#[serde(rename_all = "snake_case")]
460pub enum JoinRule {
461    /// Anyone may join without an invite.
462    Public,
463    /// Only invited users may join.
464    Invite,
465    /// Anyone may request to join (subject to approval).
466    Knock,
467    /// Only invited users may join (alias historically distinct from
468    /// `invite` in some client UIs; kept distinct per spec, not collapsed).
469    Private,
470    /// Anyone satisfying one of `allow`'s conditions may join without an
471    /// invite.
472    Restricted,
473    /// Like `restricted`, but falls back to `knock` for users not
474    /// satisfying any `allow` condition.
475    KnockRestricted,
476    /// A join rule value this crate does not recognize yet.
477    #[serde(other)]
478    Unknown,
479}
480
481/// One condition in `m.room.join_rules`'s `allow` list (used by `restricted`
482/// / `knock_restricted`).
483#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
484pub struct JoinRuleAllow {
485    /// The condition's type (spec currently defines only
486    /// `"m.room_membership"`).
487    #[serde(rename = "type")]
488    pub allow_type: String,
489    /// For `"m.room_membership"`: membership in this room satisfies the
490    /// condition.
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub room_id: Option<RoomId>,
493}
494
495/// `m.room.join_rules` content.
496#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
497pub struct RoomJoinRulesContent {
498    /// The room's join rule.
499    pub join_rule: JoinRule,
500    /// Conditions under which a `restricted`/`knock_restricted` room may be
501    /// joined without an explicit invite.
502    #[serde(default, skip_serializing_if = "Vec::is_empty")]
503    pub allow: Vec<JoinRuleAllow>,
504}
505
506/// `m.room.history_visibility`'s `history_visibility` value.
507#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
508#[serde(rename_all = "snake_case")]
509pub enum HistoryVisibility {
510    /// Visible to anyone invited, from the point they were invited onward.
511    Invited,
512    /// Visible to anyone joined, from the point they joined onward.
513    Joined,
514    /// Visible to anyone joined, including history from before they
515    /// joined.
516    Shared,
517    /// Visible to anyone, even without joining.
518    WorldReadable,
519    /// A value this crate does not recognize yet.
520    #[serde(other)]
521    Unknown,
522}
523
524/// `m.room.history_visibility` content.
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
526pub struct RoomHistoryVisibilityContent {
527    /// The room's history visibility.
528    pub history_visibility: HistoryVisibility,
529}
530
531/// `m.room.name` content.
532#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
533pub struct RoomNameContent {
534    /// The room's display name.
535    pub name: String,
536}
537
538/// `m.room.topic` content.
539#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
540pub struct RoomTopicContent {
541    /// The room's topic.
542    pub topic: String,
543}
544
545/// `m.room.encryption` content — once set on a room, this event is never
546/// removed or changed to a different algorithm (spec invariant; this crate
547/// does not enforce that, it only models the shape).
548#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
549pub struct RoomEncryptionContent {
550    /// The room's encryption algorithm (spec currently defines only
551    /// `"m.megolm.v1.aes-sha2"`).
552    pub algorithm: String,
553    /// How long an outbound Megolm session may live before rotation, in
554    /// milliseconds. Default one week.
555    #[serde(default = "default_rotation_period_ms")]
556    pub rotation_period_ms: i64,
557    /// How many messages an outbound Megolm session may encrypt before
558    /// rotation. Default `100`.
559    #[serde(default = "default_rotation_period_msgs")]
560    pub rotation_period_msgs: i64,
561}
562
563/// `m.room.pinned_events` content.
564#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
565pub struct RoomPinnedEventsContent {
566    /// The pinned event ids, in the order they were pinned.
567    pub pinned: Vec<EventId>,
568}
569
570// ---------------------------------------------------------------------
571// Timeline event content (research §1.3, §3.1)
572// ---------------------------------------------------------------------
573
574/// The shared `m.text`/`m.notice`/`m.emote` shape: a plain body plus an
575/// optional formatted (HTML) rendering.
576#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
577pub struct TextLikeMessageContent {
578    /// The plain-text body.
579    pub body: String,
580    /// The formatting used by `formatted_body` (spec currently defines only
581    /// `"org.matrix.custom.html"`), if any.
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub format: Option<String>,
584    /// The formatted rendering of `body`, if any.
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub formatted_body: Option<String>,
587}
588
589/// `m.room.message` content. `msgtype` selects the shape; an `msgtype` this
590/// crate does not model is kept as [`RoomMessageContent::Unknown`] rather
591/// than failing to parse (module doc).
592#[derive(Clone, Debug, PartialEq)]
593pub enum RoomMessageContent {
594    /// `msgtype: "m.text"`.
595    Text(TextLikeMessageContent),
596    /// `msgtype: "m.notice"` — like `m.text`, but conventionally suppressed
597    /// from push notifications (bot/automation output).
598    Notice(TextLikeMessageContent),
599    /// `msgtype: "m.emote"` — an "/me" action message.
600    Emote(TextLikeMessageContent),
601    /// Any other `msgtype` (`m.image`, `m.file`, ...), kept as the raw
602    /// content value.
603    Unknown(serde_json::Value),
604}
605
606impl Serialize for RoomMessageContent {
607    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
608    where
609        S: serde::Serializer,
610    {
611        match self {
612            RoomMessageContent::Text(inner) => serialize_tagged(serializer, "msgtype", "m.text", inner),
613            RoomMessageContent::Notice(inner) => serialize_tagged(serializer, "msgtype", "m.notice", inner),
614            RoomMessageContent::Emote(inner) => serialize_tagged(serializer, "msgtype", "m.emote", inner),
615            RoomMessageContent::Unknown(value) => value.serialize(serializer),
616        }
617    }
618}
619
620impl<'de> Deserialize<'de> for RoomMessageContent {
621    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
622    where
623        D: serde::Deserializer<'de>,
624    {
625        let value = serde_json::Value::deserialize(deserializer)?;
626        let msgtype = value.get("msgtype").and_then(serde_json::Value::as_str).map(str::to_owned);
627        match msgtype.as_deref() {
628            Some("m.text") => serde_json::from_value(value).map(RoomMessageContent::Text).map_err(serde::de::Error::custom),
629            Some("m.notice") => {
630                serde_json::from_value(value).map(RoomMessageContent::Notice).map_err(serde::de::Error::custom)
631            }
632            Some("m.emote") => {
633                serde_json::from_value(value).map(RoomMessageContent::Emote).map_err(serde::de::Error::custom)
634            }
635            _ => Ok(RoomMessageContent::Unknown(value)),
636        }
637    }
638}
639
640/// `m.room.encrypted`'s Megolm shape (`algorithm:
641/// "m.megolm.v1.aes-sha2"`) — the only algorithm ever used for a *timeline*
642/// `m.room.encrypted` event (research doc §3.1). `sender_key`/`device_id`
643/// are spec-deprecated but still sent for back-compat. `Eq` is not derived
644/// here (unlike this struct's siblings): `relates_to`'s `RelatesTo` type
645/// only implements `PartialEq` (its `Unknown` variant carries a
646/// `serde_json::Value`), so this struct follows suit.
647#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
648pub struct MegolmEncryptedContent {
649    /// The Megolm ciphertext, base64-encoded.
650    pub ciphertext: String,
651    /// The Megolm session this was encrypted with.
652    pub session_id: String,
653    /// The sender device's Curve25519 identity key (deprecated, still
654    /// sent).
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub sender_key: Option<String>,
657    /// The sender device's id (deprecated, still sent).
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub device_id: Option<DeviceId>,
660    /// Copied verbatim from the cleartext plaintext's own `m.relates_to`
661    /// (Megolm-encrypted rooms binding rule): the server indexes
662    /// relations (reactions, edits, threads) without ever decrypting, so
663    /// the relation pointer rides alongside the ciphertext at the outer,
664    /// unencrypted envelope level too, not only inside the plaintext this
665    /// crate's `crypto::group_sessions` encrypts.
666    #[serde(rename = "m.relates_to", default, skip_serializing_if = "Option::is_none")]
667    pub relates_to: Option<RelatesTo>,
668}
669
670/// One recipient device's ciphertext inside an Olm `m.room.encrypted`
671/// envelope's `ciphertext` map.
672#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
673pub struct OlmCiphertextInfo {
674    /// The Olm message type (`0` = pre-key message, `1` = normal message).
675    #[serde(rename = "type")]
676    pub message_type: u8,
677    /// The Olm ciphertext, base64-encoded.
678    pub body: String,
679}
680
681/// `m.room.encrypted`'s Olm shape (`algorithm:
682/// "m.olm.v1.curve25519-aes-sha2"`) — used only for to-device delivery
683/// (room keys, verification, ...), never in a room timeline (research doc
684/// §3.1).
685#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
686pub struct OlmEncryptedContent {
687    /// The sender device's Curve25519 identity key.
688    pub sender_key: String,
689    /// Ciphertext keyed by each recipient device's Curve25519 identity key.
690    pub ciphertext: BTreeMap<String, OlmCiphertextInfo>,
691}
692
693/// `m.room.encrypted` content, either shape research doc §3.1 describes. An
694/// `algorithm` this crate does not recognize is kept as
695/// [`RoomEncryptedContent::Unknown`] (module doc).
696#[derive(Clone, Debug, PartialEq)]
697pub enum RoomEncryptedContent {
698    /// `algorithm: "m.megolm.v1.aes-sha2"`.
699    Megolm(MegolmEncryptedContent),
700    /// `algorithm: "m.olm.v1.curve25519-aes-sha2"`.
701    Olm(OlmEncryptedContent),
702    /// Any other `algorithm`, kept as the raw content value.
703    Unknown(serde_json::Value),
704}
705
706impl Serialize for RoomEncryptedContent {
707    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
708    where
709        S: serde::Serializer,
710    {
711        match self {
712            RoomEncryptedContent::Megolm(inner) => {
713                serialize_tagged(serializer, "algorithm", "m.megolm.v1.aes-sha2", inner)
714            }
715            RoomEncryptedContent::Olm(inner) => {
716                serialize_tagged(serializer, "algorithm", "m.olm.v1.curve25519-aes-sha2", inner)
717            }
718            RoomEncryptedContent::Unknown(value) => value.serialize(serializer),
719        }
720    }
721}
722
723impl<'de> Deserialize<'de> for RoomEncryptedContent {
724    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
725    where
726        D: serde::Deserializer<'de>,
727    {
728        let value = serde_json::Value::deserialize(deserializer)?;
729        let algorithm = value.get("algorithm").and_then(serde_json::Value::as_str).map(str::to_owned);
730        match algorithm.as_deref() {
731            Some("m.megolm.v1.aes-sha2") => {
732                serde_json::from_value(value).map(RoomEncryptedContent::Megolm).map_err(serde::de::Error::custom)
733            }
734            Some("m.olm.v1.curve25519-aes-sha2") => {
735                serde_json::from_value(value).map(RoomEncryptedContent::Olm).map_err(serde::de::Error::custom)
736            }
737            _ => Ok(RoomEncryptedContent::Unknown(value)),
738        }
739    }
740}
741
742/// `m.reaction` content: an annotation-relation, key is typically an emoji.
743#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
744pub struct ReactionContent {
745    /// The annotation this reaction carries.
746    #[serde(rename = "m.relates_to")]
747    pub relates_to: RelatesTo,
748}
749
750/// `m.room.redaction` content (room v11: `redacts` moved from the event's
751/// top level into `content`, server plan manager decision #1).
752#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
753pub struct RedactionContent {
754    /// The event being redacted.
755    pub redacts: EventId,
756    /// Why the event was redacted, if given.
757    #[serde(default, skip_serializing_if = "Option::is_none")]
758    pub reason: Option<String>,
759}
760
761// ---------------------------------------------------------------------
762// To-device payloads (research §3.1, §3.3)
763// ---------------------------------------------------------------------
764
765/// `m.room_key`: an Olm-to-device-delivered Megolm session key (research
766/// doc §3.1). Never appears in a room timeline.
767#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
768pub struct RoomKeyContent {
769    /// The Megolm algorithm (`"m.megolm.v1.aes-sha2"`).
770    pub algorithm: String,
771    /// The room this session key is for.
772    pub room_id: RoomId,
773    /// The Megolm session id.
774    pub session_id: String,
775    /// The Megolm session key, base64-encoded.
776    pub session_key: String,
777}
778
779/// `m.forwarded_room_key`: like [`RoomKeyContent`], plus provenance fields
780/// so the recipient's client can show the "this key was shared with you"
781/// warning research doc §3.3 describes.
782#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
783pub struct ForwardedRoomKeyContent {
784    /// The Megolm algorithm (`"m.megolm.v1.aes-sha2"`).
785    pub algorithm: String,
786    /// The room this session key is for.
787    pub room_id: RoomId,
788    /// The Megolm session id.
789    pub session_id: String,
790    /// The Megolm session key, base64-encoded.
791    pub session_key: String,
792    /// The original sender device's Curve25519 identity key.
793    pub sender_key: String,
794    /// The original sender device's claimed Ed25519 signing key, if known.
795    #[serde(default, skip_serializing_if = "Option::is_none")]
796    pub sender_claimed_ed25519_key: Option<String>,
797    /// Every device's Curve25519 key this key was forwarded through, in
798    /// order, oldest first.
799    #[serde(default)]
800    pub forwarding_curve25519_key_chain: Vec<String>,
801}
802
803/// `m.room_key_request`'s `action` value.
804#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "snake_case")]
806pub enum RoomKeyRequestAction {
807    /// Ask a peer device to (re-)share a session key.
808    Request,
809    /// Withdraw an earlier `request`.
810    RequestCancellation,
811    /// An action value this crate does not recognize yet.
812    #[serde(other)]
813    Unknown,
814}
815
816/// `m.room_key_request`'s `body` field: which session is being requested.
817/// Absent for a `request_cancellation`.
818#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
819pub struct RoomKeyRequestBody {
820    /// The Megolm algorithm (`"m.megolm.v1.aes-sha2"`).
821    pub algorithm: String,
822    /// The room the requested session is for.
823    pub room_id: RoomId,
824    /// The requested Megolm session id.
825    pub session_id: String,
826    /// The requested session's sender device Curve25519 identity key.
827    pub sender_key: String,
828}
829
830/// `m.room_key_request` content (research doc §3.3). Answered for a joined
831/// or invited member of the room, and for this account's other devices.
832/// Device verification is not required: nothing in this deployment marks a
833/// device verified, and the peer who sent the Megolm event is who holds
834/// the session.
835#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
836pub struct RoomKeyRequestContent {
837    /// Whether this is a request or a cancellation of an earlier one.
838    pub action: RoomKeyRequestAction,
839    /// Which session is being requested; absent for a cancellation.
840    #[serde(default, skip_serializing_if = "Option::is_none")]
841    pub body: Option<RoomKeyRequestBody>,
842    /// The requester's own id for this request, echoed back on
843    /// cancellation.
844    pub request_id: String,
845    /// The requesting device.
846    pub requesting_device_id: DeviceId,
847}
848
849/// `m.room_key.withheld`'s `code` value — why a device did not (or will
850/// not) receive a requested session key. An unrecognized code is kept
851/// verbatim in [`WithheldCode::Unknown`] rather than failing to parse
852/// (module doc).
853#[derive(Clone, Debug, PartialEq, Eq)]
854pub enum WithheldCode {
855    /// The requesting device is on the sender's block list.
856    Blacklisted,
857    /// The requesting device has not been verified.
858    Unverified,
859    /// The requesting user is not authorized to receive this key.
860    Unauthorised,
861    /// No session exists yet to share.
862    Unavailable,
863    /// The sender could not establish an Olm session with the requester.
864    NoOlm,
865    /// A code this crate does not recognize yet, kept verbatim.
866    Unknown(String),
867}
868
869impl WithheldCode {
870    /// This code's own wire string.
871    pub fn as_str(&self) -> &str {
872        match self {
873            WithheldCode::Blacklisted => "m.blacklisted",
874            WithheldCode::Unverified => "m.unverified",
875            WithheldCode::Unauthorised => "m.unauthorised",
876            WithheldCode::Unavailable => "m.unavailable",
877            WithheldCode::NoOlm => "m.no_olm",
878            WithheldCode::Unknown(raw) => raw.as_str(),
879        }
880    }
881}
882
883impl Serialize for WithheldCode {
884    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
885    where
886        S: serde::Serializer,
887    {
888        serializer.serialize_str(self.as_str())
889    }
890}
891
892impl<'de> Deserialize<'de> for WithheldCode {
893    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
894    where
895        D: serde::Deserializer<'de>,
896    {
897        let raw = String::deserialize(deserializer)?;
898        Ok(match raw.as_str() {
899            "m.blacklisted" => WithheldCode::Blacklisted,
900            "m.unverified" => WithheldCode::Unverified,
901            "m.unauthorised" => WithheldCode::Unauthorised,
902            "m.unavailable" => WithheldCode::Unavailable,
903            "m.no_olm" => WithheldCode::NoOlm,
904            _ => WithheldCode::Unknown(raw),
905        })
906    }
907}
908
909/// `m.room_key.withheld` content (research doc §3.3).
910#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
911pub struct RoomKeyWithheldContent {
912    /// The Megolm algorithm (`"m.megolm.v1.aes-sha2"`).
913    pub algorithm: String,
914    /// The room the withheld session was for, if the sender chose to say.
915    #[serde(default, skip_serializing_if = "Option::is_none")]
916    pub room_id: Option<RoomId>,
917    /// The withheld Megolm session id, if the sender chose to say.
918    #[serde(default, skip_serializing_if = "Option::is_none")]
919    pub session_id: Option<String>,
920    /// The sender device's Curve25519 identity key.
921    pub sender_key: String,
922    /// Why the key was withheld.
923    pub code: WithheldCode,
924    /// A human-readable explanation, if given.
925    #[serde(default, skip_serializing_if = "Option::is_none")]
926    pub reason: Option<String>,
927}
928
929/// `m.dummy` content — always empty; sent purely to advance an Olm ratchet
930/// without a meaningful payload.
931#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
932pub struct DummyContent {}
933
934// ---------------------------------------------------------------------
935// Account data (research §1.6, §1.7)
936// ---------------------------------------------------------------------
937
938/// `m.direct` content: direct-message room ids, keyed by the peer user.
939#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
940#[serde(transparent)]
941pub struct DirectContent(pub BTreeMap<UserId, Vec<RoomId>>);
942
943/// One entry in `m.tag`'s `tags` map.
944#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
945pub struct TagInfo {
946    /// Sort order among sibling tags, lower sorts first. No fixed range or
947    /// uniqueness requirement per spec.
948    #[serde(default, skip_serializing_if = "Option::is_none")]
949    pub order: Option<f64>,
950}
951
952/// `m.tag` content: per-room, per-user tags.
953#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
954pub struct TagContent {
955    /// Tag name → tag metadata.
956    #[serde(default)]
957    pub tags: BTreeMap<String, TagInfo>,
958}
959
960/// `m.fully_read` content, set via `/rooms/{roomId}/read_markers` (research
961/// doc §1.6): the "read up to here for UI purposes" position, decoupled
962/// from the receipt broadcast.
963#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
964pub struct FullyReadContent {
965    /// The last event the user has read, for UI purposes.
966    pub event_id: EventId,
967}
968
969// ---------------------------------------------------------------------
970// Ephemeral (research §1.6)
971// ---------------------------------------------------------------------
972
973/// `m.typing` content: one merged event per room per `/sync` (research doc
974/// §1.6), not a discrete event per keystroke.
975#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
976pub struct TypingContent {
977    /// Every user currently typing in the room.
978    #[serde(default)]
979    pub user_ids: Vec<UserId>,
980}
981
982/// One user's receipt entry inside [`ReceiptContent`]'s nested map.
983#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
984pub struct ReceiptEntry {
985    /// Milliseconds since the Unix epoch when the receipt was sent.
986    pub ts: i64,
987    /// The thread this receipt applies to, if the client scoped it to one
988    /// (unscoped receipts apply to the room's main timeline).
989    #[serde(default, skip_serializing_if = "Option::is_none")]
990    pub thread_id: Option<String>,
991}
992
993/// `m.receipt` content: `event_id -> receipt_type -> user_id -> entry`
994/// (research doc §1.6) — one coalesced state per room per sync tick, not a
995/// stream of discrete "read" packets.
996#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
997#[serde(transparent)]
998pub struct ReceiptContent(pub BTreeMap<EventId, BTreeMap<String, BTreeMap<UserId, ReceiptEntry>>>);
999
1000#[cfg(test)]
1001mod tests {
1002    use super::*;
1003
1004    #[test]
1005    fn room_create_content_decodes_without_a_creator_field() {
1006        // Room version 11 has no `creator`; the pre-v11 shape still decodes too.
1007        let v11: RoomCreateContent = serde_json::from_value(serde_json::json!({ "room_version": "11" })).expect("v11 create content decodes");
1008        assert_eq!(v11.creator, None);
1009        assert_eq!(v11.room_version, "11");
1010        let v10: RoomCreateContent =
1011            serde_json::from_value(serde_json::json!({ "creator": "@a:example.org", "room_version": "10" })).expect("v10 create content decodes");
1012        assert_eq!(v10.creator.as_ref().map(UserId::as_str), Some("@a:example.org"));
1013    }
1014
1015    #[test]
1016    fn parses_relates_to_annotation_replace_thread() {
1017        let annotation = serde_json::json!({
1018            "m.relates_to": { "rel_type": "m.annotation", "event_id": "$target:example.org", "key": "👍" }
1019        });
1020        match RelatesTo::from_content(&annotation) {
1021            Some(RelatesTo::Annotation { event_id, key }) => {
1022                assert_eq!(event_id.as_str(), "$target:example.org");
1023                assert_eq!(key, "👍");
1024            }
1025            other => panic!("expected Annotation, got {other:?}"),
1026        }
1027
1028        let replace = serde_json::json!({
1029            "m.relates_to": { "rel_type": "m.replace", "event_id": "$original:example.org" },
1030            "m.new_content": { "msgtype": "m.text", "body": "edited" }
1031        });
1032        match RelatesTo::from_content(&replace) {
1033            Some(RelatesTo::Replace { event_id }) => assert_eq!(event_id.as_str(), "$original:example.org"),
1034            other => panic!("expected Replace, got {other:?}"),
1035        }
1036
1037        let thread = serde_json::json!({
1038            "m.relates_to": {
1039                "rel_type": "m.thread",
1040                "event_id": "$root:example.org",
1041                "is_falling_back": true,
1042                "m.in_reply_to": { "event_id": "$latest:example.org" }
1043            }
1044        });
1045        match RelatesTo::from_content(&thread) {
1046            Some(RelatesTo::Thread { event_id, is_falling_back, in_reply_to }) => {
1047                assert_eq!(event_id.as_str(), "$root:example.org");
1048                assert!(is_falling_back);
1049                assert_eq!(in_reply_to.expect("in_reply_to present").event_id.as_str(), "$latest:example.org");
1050            }
1051            other => panic!("expected Thread, got {other:?}"),
1052        }
1053
1054        let legacy_reply = serde_json::json!({
1055            "m.relates_to": { "m.in_reply_to": { "event_id": "$legacy:example.org" } }
1056        });
1057        match RelatesTo::from_content(&legacy_reply) {
1058            Some(RelatesTo::InReplyTo(reply)) => assert_eq!(reply.event_id.as_str(), "$legacy:example.org"),
1059            other => panic!("expected InReplyTo, got {other:?}"),
1060        }
1061
1062        let no_relation = serde_json::json!({ "msgtype": "m.text", "body": "plain" });
1063        assert_eq!(RelatesTo::from_content(&no_relation), None);
1064    }
1065
1066    #[test]
1067    fn unrecognized_relation_shape_becomes_unknown_not_a_parse_failure() {
1068        let content = serde_json::json!({ "m.relates_to": { "rel_type": "org.example.future_relation", "foo": "bar" } });
1069        match RelatesTo::from_content(&content) {
1070            Some(RelatesTo::Unknown(value)) => {
1071                assert_eq!(value["rel_type"], "org.example.future_relation");
1072            }
1073            other => panic!("expected Unknown, got {other:?}"),
1074        }
1075    }
1076
1077    #[test]
1078    fn unknown_event_type_is_kept_raw() {
1079        let json = serde_json::json!({
1080            "event_id": "$abc:example.org",
1081            "type": "org.example.made_up_event",
1082            "sender": "@alice:example.org",
1083            "origin_server_ts": 1_700_000_000_000i64,
1084            "content": { "whatever": ["this", "is"], "n": 3 },
1085            "unsigned": { "age": 12 }
1086        });
1087        let event: RawEvent = serde_json::from_value(json).expect("unrecognized type still parses");
1088        assert_eq!(event.event_type, "org.example.made_up_event");
1089        assert_eq!(event.content["whatever"][0], "this");
1090        assert_eq!(event.content["n"], 3);
1091        assert_eq!(event.unsigned.age, Some(12));
1092        assert_eq!(event.state_key, None);
1093    }
1094
1095    #[test]
1096    fn redacted_because_nests_the_redaction_event() {
1097        let json = serde_json::json!({
1098            "event_id": "$abc:example.org",
1099            "type": "m.room.message",
1100            "sender": "@alice:example.org",
1101            "origin_server_ts": 1,
1102            "content": {},
1103            "unsigned": {
1104                "redacted_because": {
1105                    "event_id": "$redaction:example.org",
1106                    "type": "m.room.redaction",
1107                    "sender": "@mod:example.org",
1108                    "origin_server_ts": 2,
1109                    "content": { "redacts": "$abc:example.org" }
1110                }
1111            }
1112        });
1113        let event: RawEvent = serde_json::from_value(json).expect("valid event");
1114        let redaction = event.unsigned.redacted_because.expect("redacted_because present");
1115        assert_eq!(redaction.event_type, "m.room.redaction");
1116        let redaction_content: RedactionContent =
1117            serde_json::from_value(redaction.content.clone()).expect("valid redaction content");
1118        assert_eq!(redaction_content.redacts.as_str(), "$abc:example.org");
1119    }
1120
1121    #[test]
1122    fn encrypted_content_parses_megolm_and_olm_shapes() {
1123        let megolm = serde_json::json!({
1124            "algorithm": "m.megolm.v1.aes-sha2",
1125            "ciphertext": "cipher-bytes-b64",
1126            "session_id": "session-1",
1127            "sender_key": "sender-curve25519",
1128            "device_id": "DEVICE1"
1129        });
1130        match serde_json::from_value::<RoomEncryptedContent>(megolm).expect("valid megolm content") {
1131            RoomEncryptedContent::Megolm(inner) => {
1132                assert_eq!(inner.ciphertext, "cipher-bytes-b64");
1133                assert_eq!(inner.session_id, "session-1");
1134                assert_eq!(inner.sender_key.as_deref(), Some("sender-curve25519"));
1135                assert_eq!(inner.device_id.map(|d| d.as_str().to_string()), Some("DEVICE1".to_string()));
1136            }
1137            other => panic!("expected Megolm, got {other:?}"),
1138        }
1139
1140        let olm = serde_json::json!({
1141            "algorithm": "m.olm.v1.curve25519-aes-sha2",
1142            "sender_key": "sender-curve25519",
1143            "ciphertext": {
1144                "recipient-curve25519": { "type": 0, "body": "prekey-message-b64" }
1145            }
1146        });
1147        match serde_json::from_value::<RoomEncryptedContent>(olm).expect("valid olm content") {
1148            RoomEncryptedContent::Olm(inner) => {
1149                assert_eq!(inner.sender_key, "sender-curve25519");
1150                let entry = inner.ciphertext.get("recipient-curve25519").expect("recipient entry present");
1151                assert_eq!(entry.message_type, 0);
1152                assert_eq!(entry.body, "prekey-message-b64");
1153            }
1154            other => panic!("expected Olm, got {other:?}"),
1155        }
1156
1157        let unknown = serde_json::json!({ "algorithm": "org.example.future-algorithm", "opaque": true });
1158        match serde_json::from_value::<RoomEncryptedContent>(unknown).expect("unrecognized algorithm still parses") {
1159            RoomEncryptedContent::Unknown(value) => assert_eq!(value["opaque"], true),
1160            other => panic!("expected Unknown, got {other:?}"),
1161        }
1162    }
1163
1164    #[test]
1165    fn room_message_unknown_msgtype_is_kept_raw() {
1166        let image = serde_json::json!({ "msgtype": "m.image", "body": "cat.png", "url": "mxc://example.org/abc" });
1167        match serde_json::from_value::<RoomMessageContent>(image).expect("unrecognized msgtype still parses") {
1168            RoomMessageContent::Unknown(value) => assert_eq!(value["url"], "mxc://example.org/abc"),
1169            other => panic!("expected Unknown, got {other:?}"),
1170        }
1171
1172        let text = serde_json::json!({ "msgtype": "m.text", "body": "hi", "formatted_body": "<b>hi</b>", "format": "org.matrix.custom.html" });
1173        match serde_json::from_value::<RoomMessageContent>(text).expect("valid text content") {
1174            RoomMessageContent::Text(inner) => {
1175                assert_eq!(inner.body, "hi");
1176                assert_eq!(inner.formatted_body.as_deref(), Some("<b>hi</b>"));
1177            }
1178            other => panic!("expected Text, got {other:?}"),
1179        }
1180    }
1181
1182    #[test]
1183    fn power_levels_defaults_apply() {
1184        let content: RoomPowerLevelsContent = serde_json::from_value(serde_json::json!({})).expect("empty object parses");
1185        assert_eq!(content.ban, 50);
1186        assert_eq!(content.invite, 50);
1187        assert_eq!(content.kick, 50);
1188        assert_eq!(content.redact, 50);
1189        assert_eq!(content.state_default, 50);
1190        assert_eq!(content.events_default, 0);
1191        assert_eq!(content.users_default, 0);
1192        assert_eq!(content.notifications.room, 50);
1193        assert!(content.events.is_empty());
1194        assert!(content.users.is_empty());
1195    }
1196
1197    #[test]
1198    fn power_levels_overrides_the_spec_defaults() {
1199        let content: RoomPowerLevelsContent = serde_json::from_value(serde_json::json!({
1200            "ban": 100,
1201            "users": { "@admin:example.org": 100 },
1202            "events": { "m.room.name": 50 }
1203        }))
1204        .expect("valid power levels");
1205        assert_eq!(content.ban, 100);
1206        let admin = UserId::parse("@admin:example.org").expect("valid user id");
1207        assert_eq!(content.users.get(&admin), Some(&100));
1208        assert_eq!(content.events.get("m.room.name"), Some(&50));
1209        // Untouched fields keep the spec default.
1210        assert_eq!(content.invite, 50);
1211    }
1212
1213    #[test]
1214    fn receipt_content_parses_nested_map() {
1215        let json = serde_json::json!({
1216            "$event1:example.org": {
1217                "m.read": {
1218                    "@alice:example.org": { "ts": 1_436_451_550_453i64, "thread_id": "main" }
1219                },
1220                "m.read.private": {
1221                    "@alice:example.org": { "ts": 1_436_451_551_000i64 }
1222                }
1223            }
1224        });
1225        let receipts: ReceiptContent = serde_json::from_value(json).expect("valid receipt content");
1226        let event_id = EventId::parse("$event1:example.org").expect("valid event id");
1227        let alice = UserId::parse("@alice:example.org").expect("valid user id");
1228        let by_type = receipts.0.get(&event_id).expect("event present");
1229        let read = by_type.get("m.read").expect("m.read present").get(&alice).expect("alice present");
1230        assert_eq!(read.ts, 1_436_451_550_453);
1231        assert_eq!(read.thread_id.as_deref(), Some("main"));
1232        let read_private =
1233            by_type.get("m.read.private").expect("m.read.private present").get(&alice).expect("alice present");
1234        assert_eq!(read_private.ts, 1_436_451_551_000);
1235        assert_eq!(read_private.thread_id, None);
1236    }
1237
1238    #[test]
1239    fn withheld_codes_parse_including_unknown() {
1240        let cases = [
1241            ("m.blacklisted", WithheldCode::Blacklisted),
1242            ("m.unverified", WithheldCode::Unverified),
1243            ("m.unauthorised", WithheldCode::Unauthorised),
1244            ("m.unavailable", WithheldCode::Unavailable),
1245            ("m.no_olm", WithheldCode::NoOlm),
1246        ];
1247        for (wire, expected) in cases {
1248            let parsed: WithheldCode = serde_json::from_value(serde_json::json!(wire)).expect("known code parses");
1249            assert_eq!(parsed, expected);
1250            assert_eq!(parsed.as_str(), wire);
1251        }
1252
1253        let unknown: WithheldCode =
1254            serde_json::from_value(serde_json::json!("m.some_future_code")).expect("unknown code still parses");
1255        assert_eq!(unknown, WithheldCode::Unknown("m.some_future_code".to_string()));
1256        assert_eq!(unknown.as_str(), "m.some_future_code");
1257
1258        let content: RoomKeyWithheldContent = serde_json::from_value(serde_json::json!({
1259            "algorithm": "m.megolm.v1.aes-sha2",
1260            "room_id": "!room:example.org",
1261            "session_id": "session-1",
1262            "sender_key": "sender-curve25519",
1263            "code": "m.unverified",
1264            "reason": "device not verified"
1265        }))
1266        .expect("valid withheld content");
1267        assert_eq!(content.code, WithheldCode::Unverified);
1268        assert_eq!(content.reason.as_deref(), Some("device not verified"));
1269    }
1270
1271}