arminius_lib_events/transitions.rs
1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Core `RoomEvent<S>` type and state transition logic.
6//!
7//! This module contains the main `RoomEvent<S>` generic type that enforces
8//! type-system enforced state transitions throughout the event lifecycle.
9//!
10//! 🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️🏳️⚧️
11
12use std::collections::HashMap;
13
14use ruma::{
15 MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId,
16 api::client::filter,
17 events::{
18 AnyStrippedStateEvent, StateEventType, TimelineEventType, room::member::MembershipState,
19 },
20 room_version_rules::EventIdFormatVersion,
21 serde::{Base64, Raw},
22 signatures::{Ed25519KeyPair, PublicKeyMap, PublicKeySet},
23 state_res,
24};
25use serde::{Deserialize, Serialize};
26use serde_json::{json, value::RawValue as RawJsonValue};
27
28use crate::{
29 ClientContext, ContextualizedFields as _, EventHash, EventRefs, KnownRoomVersionId,
30 VerificationMetadata,
31 conversions::{build_unsigned, extract_signatures, raw_json},
32 errors::{
33 AuthorizationError, EventContextualizationError, EventIsNotStateEvent, FromPduError,
34 SigningError, SigningValidationError, ValidationError,
35 },
36 pdu::{ClientEvent, Pdu},
37 states::{Contextualized, Draft, Signed, Verified},
38 types::StateKey,
39};
40
41/// A Matrix room event in a specific lifecycle state.
42///
43/// `RoomEvent<S>` is a generic wrapper that ensures type-safe state transitions
44/// throughout the event lifecycle. The type parameter `S` determines which
45/// methods are available and which transitions are permitted.
46///
47/// ## Type States
48///
49/// - [`RoomEvent<Draft>`]: Initial creation from user input
50/// - [`RoomEvent<Contextualized>`]: Contextualized in the room DAG
51/// - [`RoomEvent<Signed>`]: Signed with event ID and cryptographic signatures
52/// - [`RoomEvent<Verified>`]: Verified and ready for persistence/distribution
53///
54/// ## Design Philosophy
55///
56/// The state-based design enforces correctness at compile-time:
57///
58/// - You cannot sign an event without first contextualizing it
59/// - You cannot store an event without first verifying it
60/// - You cannot send an event to federation without signatures
61///
62/// This prevents entire classes of security bugs by making invalid states
63/// unrepresentable.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct RoomEvent<S> {
66 /// The state-specific data
67 pub(crate) state: S,
68}
69
70impl RoomEvent<Draft> {
71 /// Creates a draft event
72 ///
73 /// ## Errors
74 ///
75 /// Returns a `serde_json::Error` if the event content does not have the
76 /// fields expected of its type.
77 pub fn new(
78 event_type: TimelineEventType,
79 content: Box<serde_json::value::RawValue>,
80 state_key: Option<StateKey>,
81 sender: OwnedUserId,
82 redacts: Option<OwnedEventId>,
83 ) -> Result<Self, serde_json::Error> {
84 Ok(Self { state: Draft::new(event_type, content, state_key, sender, redacts)? })
85 }
86
87 /// Contextualize the draft into the room DAG to create a [`Contextualized`]
88 /// event.
89 ///
90 /// This method places the draft event in the context of the room's directed
91 /// acyclic graph by adding references to auth events and previous events,
92 /// along with server timestamp and room version information.
93 ///
94 /// ## Room Version
95 ///
96 /// The `room_version` parameter determines:
97 ///
98 /// - Event ID format (hashed vs. reference-based)
99 /// - Event reference format (V1 hashed vs. V3+ plain)
100 /// - Authorization rules
101 /// - Signature requirements
102 ///
103 /// ## Parameters
104 ///
105 /// - `room_id`: The ID of the room the event is in. If the event type is `m.room.create` and
106 /// the `room_version` is 12 or above, this should be None.
107 /// - `room_version`: Room version rules determining event format and authorization.
108 /// - `depth`: Depth of the event in the DAG.
109 /// - `auth_events`: Auth event references for authorization chain.
110 /// - `prev_events`: Previous event references establishing DAG position.
111 /// - `origin_server_ts`: Server-assigned timestamp for the event.
112 ///
113 /// ## Returns
114 ///
115 /// A [`Contextualized`] event ready to be signed.
116 pub fn contextualize(
117 self,
118 room_id: Option<OwnedRoomId>,
119 room_version: KnownRoomVersionId,
120 depth: u64,
121 auth_events: EventRefs,
122 prev_events: EventRefs,
123 origin_server_ts: MilliSecondsSinceUnixEpoch,
124 ) -> Result<RoomEvent<Contextualized>, EventContextualizationError> {
125 Ok(RoomEvent {
126 state: Contextualized::new(
127 self.state,
128 depth,
129 room_version,
130 auth_events,
131 prev_events,
132 None,
133 None,
134 None,
135 origin_server_ts,
136 None,
137 room_id,
138 None,
139 )?,
140 })
141 }
142}
143
144impl RoomEvent<Contextualized> {
145 /// Sign the event to produce a [`Verified`] event.
146 ///
147 /// Check [`VerificationMetadata::soft_failed`]
148 /// to determine whether the event should be excluded from leaf events and
149 /// client relay.
150 ///
151 /// This method performs the following operations:
152 ///
153 /// 1. Canonicalizes the event JSON according to Matrix specification
154 /// 2. Computes the reference hash to generate the event ID
155 /// 3. Signs the canonical JSON with the provided key pair
156 /// 4. Attaches the signature to the event
157 /// 5. Verifies that the event follows authorization rules
158 /// 6. Returns a [`Verified`] event (signing implicitly verifies)
159 ///
160 /// ## Implicit Verification
161 ///
162 /// Events signed by our own server are implicitly verified - we trust our
163 /// own signing process. This is why `sign()` returns [`Verified`] rather
164 /// than [`Signed`].
165 ///
166 /// ## Parameters
167 ///
168 /// - `server_name`: The origin server name (e.g., "avastar.example.com") used to namespace the
169 /// signature in the `signatures` field.
170 /// - `signing_key`: The Ed25519 key pair used to generate and verify the cryptographic
171 /// signature.
172 /// - `previous_state`: The room state corresponding to the resolution of the state of this
173 /// event's `prev_events`.
174 /// - `auth_events`: The specific auth events referenced by this event.
175 /// - `current_state`: The room state at the time the event is being
176 /// verified. If the event fails this auth check, it will be
177 /// soft-failed: the returned [`RoomEvent<Verified>`] will have
178 /// [`VerificationMetadata::soft_failed`] set to `true`. See
179 /// <https://spec.matrix.org/v1.17/server-server-api/#soft-failure> for more information.
180 ///
181 /// ## Returns
182 ///
183 /// A [`RoomEvent<Verified>`] with event ID and signatures, ready for
184 /// persistence or federation.
185 ///
186 /// ## Errors
187 ///
188 /// Returns [`SigningValidationError`] if:
189 ///
190 /// - Event content is not a valid JSON object
191 /// - Canonical JSON generation fails
192 /// - Reference hash computation fails
193 /// - Cryptographic signing fails
194 /// - Event ID parsing fails
195 /// - Event fails authorization rules
196 pub fn sign_and_verify(
197 self,
198 server_name: &ruma::ServerName,
199 signing_key: &Ed25519KeyPair,
200 previous_state: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
201 auth_events: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
202 current_state: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
203 ) -> Result<RoomEvent<Verified>, SigningValidationError> {
204 let event = self.sign_event(server_name, signing_key)?;
205 let event = RoomEvent { state: event.state.signed };
206
207 let verify_key_map = PublicKeyMap::from([(
208 server_name.to_string(),
209 PublicKeySet::from([(
210 format!("ed25519:{}", signing_key.version()),
211 Base64::new(signing_key.public_key().to_vec()),
212 )]),
213 )]);
214
215 event
216 .verify(previous_state, auth_events, current_state, &verify_key_map)
217 .map_err(SigningValidationError::ValidationError)
218 }
219
220 /// Sign a membership event, producing a PDU to send over federation
221 /// This method only accepts membership events. It's intended for use
222 /// when joining rooms over federation, as specified in
223 /// <https://spec.matrix.org/v1.18/server-server-api/#joining-rooms>
224 ///
225 /// The events produced by this method should **NOT** be persisted directly
226 /// in the database. Instead, they should be sent over federation with the
227 /// `/send_join` request, where we as a response will get the state events
228 /// required to check the authorization rules.
229 ///
230 /// This method performs the following operations:
231 ///
232 /// 1. Canonicalizes the event JSON according to Matrix specification
233 /// 2. Computes the reference hash to generate the event ID
234 /// 3. Signs the canonical JSON with the provided key pair
235 /// 4. Attaches the signature to the event
236 /// 5. Returns a [`Pdu`]
237 ///
238 /// ## Parameters
239 ///
240 /// - `server_name`: The origin server name (e.g., "avastar.example.com") used to namespace the
241 /// signature in the `signatures` field.
242 /// - `signing_key`: The Ed25519 key pair used to generate and verify the cryptographic
243 /// signature.
244 ///
245 /// ## Returns
246 ///
247 /// A [`Pdu`] to be sent to another homeserver to counter-sign
248 ///
249 /// ## Errors
250 ///
251 /// Returns [`SigningError`] if:
252 ///
253 /// - Event content is not a valid JSON object
254 /// - Event is not a membership event
255 /// - Canonical JSON generation fails
256 /// - Reference hash computation fails
257 /// - Cryptographic signing fails
258 /// - Event ID parsing fails
259 pub fn sign_join_event(
260 self,
261 server_name: &ruma::ServerName,
262 signing_key: &Ed25519KeyPair,
263 ) -> Result<Pdu, SigningError> {
264 if self.state.draft.timeline_event_type != TimelineEventType::RoomMember {
265 return Err(SigningError::InvalidContent(
266 "Refusing to sign non-member event".to_owned(),
267 ));
268 }
269 let event = self.sign_event(server_name, signing_key)?;
270 Ok(event.to_pdu())
271 }
272
273 /// Signs the event
274 fn sign_event(
275 self,
276 server_name: &ruma::ServerName,
277 signing_key: &Ed25519KeyPair,
278 ) -> Result<RoomEvent<Verified>, SigningError> {
279 let room_version_rules = &self.state.room_version.rules();
280 let mut canonical = self.state.build_canonical_event_object()?;
281
282 // For room versions where event_id is part of the PDU (V1/V2 rooms):
283 // the redaction algorithm retains `event_id` in the signed form, so
284 // it must be inserted into the canonical before hash_and_sign_event
285 if room_version_rules.event_id_format == EventIdFormatVersion::V1 {
286 canonical.insert(
287 "event_id".to_owned(),
288 ruma::CanonicalJsonValue::String(self.event_id().to_string()),
289 );
290 }
291
292 canonical.insert("hashes".to_owned(), self.hashes().to_canonical_json_value());
293
294 ruma::signatures::sign_event(
295 server_name.as_str(),
296 signing_key,
297 &mut canonical,
298 &room_version_rules.redaction,
299 )
300 .map_err(|error| SigningError::SignatureCreation(error.to_string()))?;
301
302 let signatures = extract_signatures(&canonical)?;
303
304 let metadata = VerificationMetadata {
305 attempted_at: MilliSecondsSinceUnixEpoch::now(),
306 signature_verification: crate::types::PartsVerified::CompleteEvent,
307 room_version: self.state.room_version,
308 soft_failed: false,
309 };
310
311 let signed = Signed { contextualized: self.state, signatures };
312
313 let event = RoomEvent { state: Verified { signed, metadata } };
314 Ok(event)
315 }
316}
317
318impl RoomEvent<Signed> {
319 /// Create a new signed event (e.g. from federation)
320 ///
321 /// ## Parameters
322 ///
323 /// - `content`: Raw JSON content of the event.
324 /// - `depth`: Depth of the event in the DAG.
325 /// - `event_type`: Matrix event type.
326 /// - `room_id`: Room ID the event belongs to.
327 /// - `sender`: User ID that sent the event.
328 /// - `state_key`: State key for state events.
329 /// - `redacts`: Event ID that is redacted, if applicable.
330 /// - `room_version`: Room version for the event.
331 /// - `auth_events`: Auth event references required for authorization.
332 /// - `prev_events`: Previous event references preceding the event.
333 /// - `origin_server_ts`: Timestamp server assigned to the event.
334 /// - `event_id`: Event ID of the signed event, if known.
335 /// - `signatures`: Signature map of the event.
336 /// - `hashes`: Content hashes of the event.
337 ///
338 /// ## Errors
339 ///
340 /// Returns [`FromPduError`] if:
341 ///
342 /// - Event content fails draft validation
343 /// - Event refs contextualization fails
344 /// - Event ID generation fails
345 #[expect(clippy::too_many_arguments, reason = "Constructor mirrors event fields")]
346 pub fn new_signed(
347 content: Box<serde_json::value::RawValue>,
348 depth: u64,
349 event_type: TimelineEventType,
350 room_id: Option<OwnedRoomId>,
351 sender: OwnedUserId,
352 state_key: Option<String>,
353 redacts: Option<OwnedEventId>,
354 room_version: KnownRoomVersionId,
355 auth_events: EventRefs,
356 prev_events: EventRefs,
357 prev_state: Option<EventRefs>,
358 membership: Option<MembershipState>,
359 origin: Option<OwnedServerName>,
360 origin_server_ts: MilliSecondsSinceUnixEpoch,
361 event_id: Option<OwnedEventId>,
362 signatures: ruma::ServerSignatures,
363 hashes: EventHash,
364 ) -> Result<Self, FromPduError> {
365 let contextualized = Contextualized::new(
366 Draft::from_signed(event_type, content, state_key, sender, redacts)?,
367 depth,
368 room_version,
369 auth_events,
370 prev_events,
371 prev_state,
372 membership,
373 origin,
374 origin_server_ts,
375 event_id,
376 room_id,
377 Some(hashes),
378 )?;
379
380 Ok(Self { state: Signed { contextualized, signatures } })
381 }
382
383 /// Recovers the authored [`Draft`] from a signed event, discarding its DAG
384 /// context, signatures and hashes.
385 ///
386 /// Re-validates through [`Draft::new`], the same gate client events pass,
387 /// so content the tolerant federation parser accepted (e.g. a redacted
388 /// event stripped to `{}`) cannot be re-signed under our own key.
389 ///
390 /// ## Errors
391 ///
392 /// Returns a `serde_json::Error` if the content does not pass
393 /// [`Draft::new`] validation for its event type.
394 pub fn into_draft(self) -> Result<RoomEvent<Draft>, serde_json::Error> {
395 let draft = self.state.contextualized.draft;
396 RoomEvent::new(
397 draft.timeline_event_type,
398 draft.content,
399 draft.state_key,
400 draft.sender,
401 draft.redacts,
402 )
403 }
404
405 /// Sign an already signed membership event, producing a PDU to send over
406 /// federation.
407 ///
408 /// ## Errors
409 ///
410 /// Returns [`SigningError`] if the event is not a membership event or
411 /// signing fails.
412 pub fn sign_member_event(
413 self,
414 server_name: &ruma::ServerName,
415 signing_key: &Ed25519KeyPair,
416 ) -> Result<Pdu, SigningError> {
417 if self.state.contextualized.draft.timeline_event_type != TimelineEventType::RoomMember {
418 return Err(SigningError::InvalidContent(
419 "Refusing to sign non-member event".to_owned(),
420 ));
421 }
422 let room_version = self.state.contextualized.room_version;
423 let mut signatures = self.state.signatures.clone();
424 let room_event = RoomEvent { state: Contextualized::from_signed_for_resign(self.state)? };
425 let mut event = room_event.sign_event(server_name, signing_key)?;
426 signatures.append(&mut event.state.signed.signatures);
427 let event = RoomEvent {
428 state: Verified {
429 signed: Signed { signatures, ..event.state.signed },
430 metadata: VerificationMetadata {
431 attempted_at: MilliSecondsSinceUnixEpoch::now(),
432 signature_verification: crate::types::PartsVerified::CompleteEvent,
433 room_version,
434 soft_failed: false,
435 },
436 },
437 };
438 Ok(event.to_pdu())
439 }
440
441 /// Sign an event without checking authorization rules
442 ///
443 /// Only enabled in tests, because it allows you to do insecure things, and doing insecure
444 /// things should be as difficult as possible.
445 #[cfg(any(feature = "test_utils", test))]
446 pub fn sign_nonverified_event(
447 self,
448 server_name: &ruma::ServerName,
449 signing_key: &Ed25519KeyPair,
450 ) -> Result<RoomEvent<Verified>, SigningError> {
451 let room_version = self.state.contextualized.room_version;
452 let mut signatures = self.state.signatures.clone();
453 let room_event = RoomEvent { state: self.state.contextualized };
454 let mut event = room_event.sign_event(server_name, signing_key)?;
455 signatures.append(&mut event.state.signed.signatures);
456 let event = RoomEvent {
457 state: Verified {
458 signed: Signed { signatures, ..event.state.signed },
459 metadata: VerificationMetadata::example(room_version),
460 },
461 };
462 Ok(event)
463 }
464
465 /// Deserializes a federation PDU and constructs a [`RoomEvent<Signed>`].
466 ///
467 /// ## Parameters
468 ///
469 /// - `pdu`: Raw JSON PDU bytes.
470 /// - `room_version`: Room version used to validate `auth_events` and `prev_events` format in
471 /// `Contextualized::new`.
472 ///
473 /// ## Errors
474 ///
475 /// Returns [`FromPduError`] if:
476 ///
477 /// - The PDU JSON cannot be deserialized
478 /// - Event refs do not match the room version, or exceed allowed counts
479 /// - Event content fails draft validation
480 /// - Event ID generation fails
481 pub fn signed_from_pdu(
482 pdu: &serde_json::value::RawValue,
483 room_version: KnownRoomVersionId,
484 ) -> Result<Self, FromPduError> {
485 #[derive(serde::Deserialize)]
486 struct SignedFederationEvent {
487 content: Box<serde_json::value::RawValue>,
488 #[serde(rename = "type")]
489 event_type: TimelineEventType,
490 event_id: Option<OwnedEventId>,
491 depth: u64,
492 room_id: Option<OwnedRoomId>,
493 sender: OwnedUserId,
494 hashes: EventHash,
495 state_key: Option<String>,
496 redacts: Option<OwnedEventId>,
497 #[serde(deserialize_with = "EventRefs::pdu_deserialize")]
498 auth_events: EventRefs,
499 #[serde(deserialize_with = "EventRefs::pdu_deserialize")]
500 prev_events: EventRefs,
501 membership: Option<MembershipState>,
502 origin: Option<OwnedServerName>,
503 prev_state: Option<EventRefs>,
504 origin_server_ts: MilliSecondsSinceUnixEpoch,
505 signatures: ruma::ServerSignatures,
506 }
507
508 let SignedFederationEvent {
509 content,
510 event_type,
511 event_id,
512 depth,
513 room_id,
514 sender,
515 hashes,
516 state_key,
517 redacts,
518 auth_events,
519 prev_events,
520 prev_state,
521 origin_server_ts,
522 signatures,
523 membership,
524 origin,
525 } = serde_json::from_str(pdu.get())?;
526
527 Self::new_signed(
528 content,
529 depth,
530 event_type,
531 room_id,
532 sender,
533 state_key,
534 redacts,
535 room_version,
536 auth_events,
537 prev_events,
538 prev_state,
539 membership,
540 origin,
541 origin_server_ts,
542 event_id,
543 signatures,
544 hashes,
545 )
546 }
547
548 /// Validate the signature of the event, without checking auth rules
549 pub fn validate_signature(
550 &self,
551 required_verify_keys: &PublicKeyMap,
552 ) -> Result<(), ValidationError> {
553 let room_version_rules = &self.state.contextualized.room_version.rules();
554 let canonical = self
555 .state
556 .build_event_object()
557 .map_err(|error| ValidationError::JsonCanonicalization(error.to_string()))?;
558
559 ruma::signatures::verify_event(required_verify_keys, &canonical, room_version_rules)
560 .map_err(|err| ValidationError::SignatureValidation(err.to_string()))?;
561 Ok(())
562 }
563
564 /// Verify a [`Signed`] event from federation or other external source.
565 /// Yields a [`RoomEvent<Verified>`].
566 ///
567 /// This method performs the following steps:
568 ///
569 /// 1. **Signature Verification**: Cryptographically verifies all signatures in the event
570 /// against the provided `required_verify_keys`.
571 /// 2. **Authorization Check**: Validates the event against room state using ruma's
572 /// `state_res::check_state_dependent_auth_rules`.
573 ///
574 /// ## Parameters
575 ///
576 /// - `previous_state`: The room state corresponding to the resolution of the state of this
577 /// event's `prev_events`.
578 /// - `auth_events`: The specific auth events referenced by this event.
579 /// - `current_state`: The room state at the time the event is being verified. If the event
580 /// fails this auth check, it will be soft-failed: the returned [`RoomEvent<Verified>`] will
581 /// have [`VerificationMetadata::soft_failed`] set to `true`.
582 /// - `required_verify_keys`: Pre-fetched public keys used to verify the event signatures.
583 ///
584 /// ## Errors
585 ///
586 /// Returns [`ValidationError`] if:
587 ///
588 /// - **Invalid signature**: Cryptographic signature verification fails
589 /// - **Event ID mismatch**: Computed reference hash doesn't match event ID
590 /// - **Authorization fails**: Event fails room authorization rules
591 /// - **Canonical JSON error**: Cannot canonicalize the event
592 pub fn verify(
593 self,
594 previous_state: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
595 auth_events: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
596 current_state: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
597 required_verify_keys: &PublicKeyMap,
598 ) -> Result<RoomEvent<Verified>, ValidationError> {
599 self.validate_signature(required_verify_keys)?;
600
601 self.check_auth(auth_events)?; // matrix spec: Server-Server: #5.1.4
602 self.check_auth(previous_state)?; // matrix spec: Server-Server: #5.1.5
603 let soft_failed = self.check_auth(current_state).is_err(); // matrix spec: Server-Server: #5.1.6
604
605 let metadata = VerificationMetadata {
606 attempted_at: MilliSecondsSinceUnixEpoch::now(),
607 signature_verification: crate::types::PartsVerified::CompleteEvent,
608 room_version: self.state.contextualized.room_version,
609 soft_failed,
610 };
611
612 Ok(RoomEvent { state: Verified { signed: self.state, metadata } })
613 }
614
615 /// Builds a PDU representation for use in authorization checks.
616 #[must_use]
617 pub fn build_pdu_for_auth(&self) -> Pdu {
618 self.state.build_pdu()
619 }
620
621 /// Checks authorization rules for the current event
622 fn check_auth(
623 &self,
624 context_events: &HashMap<(StateEventType, StateKey), RoomEvent<Verified>>,
625 ) -> Result<(), AuthorizationError> {
626 let pdu_for_auth = self.build_pdu_for_auth();
627 let fetch_state = |event_type: &StateEventType, state_key: &str| -> Option<Pdu> {
628 context_events.get(&(event_type.clone(), state_key.to_owned())).map(RoomEvent::to_pdu)
629 };
630 state_res::check_state_dependent_auth_rules(
631 &self.state.contextualized.room_version.rules().authorization,
632 pdu_for_auth,
633 fetch_state,
634 )
635 .map_err(AuthorizationError::AuthorizationFailed)?;
636 Ok(())
637 }
638}
639
640impl RoomEvent<Verified> {
641 /// Formats the event into a `Box<RawJsonValue>` representation based on the
642 /// requested formatting.
643 ///
644 /// This method adapts the internal event state to match the required JSON
645 /// schema of the target consumer. Depending on the `event_format`
646 /// provided, it generates either:
647 ///
648 /// - A **Client-formatted event**: Strips out federation-specific metadata and injects
649 /// client-specific context (such as room state or user relations) using the provided
650 /// `ClientContext`. Suitable for the Matrix Client-Server API. For more specific context
651 /// check `to_client_event` method.
652 /// - A **Federation-formatted event**: Retains all cryptographic data (`signatures`, `hashes`)
653 /// and DAG context (`prev_events`, `auth_events`)
654 ///
655 /// ## Parameters
656 ///
657 /// * `event_format` - The target format (`filter::EventFormat::Client` or
658 /// `filter::EventFormat::Federation`).
659 /// * `ctx` - The client context required for resolving client-specific fields. Unused if
660 /// `event_format` is set to `Federation`.
661 #[must_use]
662 pub fn format_as(
663 &self,
664 event_format: &filter::EventFormat,
665 ctx: ClientContext,
666 ) -> Box<RawJsonValue> {
667 match event_format {
668 filter::EventFormat::Federation => raw_json(&self.state.signed.build_event_value()),
669 _ => self.to_client_event(ctx),
670 }
671 }
672
673 /// Create a new verified event from a PDU
674 ///
675 /// As this creates a new `Verified` event, this function is somewhat
676 /// dangerous, since it technically lets you bypass verification. Therefore,
677 /// care should be taken only to use this function on events from the
678 /// database
679 ///
680 /// ## Parameters
681 ///
682 /// - `pdu`: The PDU we're loading
683 /// - `metadata`: Verification metadata, stored from earlier
684 pub fn verified_from_pdu(
685 pdu: &serde_json::value::RawValue,
686 metadata: VerificationMetadata,
687 ) -> Result<Self, FromPduError> {
688 let signed = RoomEvent::signed_from_pdu(pdu, metadata.room_version)?;
689 Ok(Self { state: Verified { signed: signed.state, metadata } })
690 }
691
692 /// Convert the verified event to a PDU (Persistent Data Unit) for
693 /// federation.
694 ///
695 /// This method serializes the event into the PDU format required by the
696 /// Matrix federation protocol. The output format varies by room version.
697 #[must_use]
698 pub fn to_pdu(&self) -> Pdu {
699 self.state.signed.build_pdu()
700 }
701
702 /// Convert the verified event to a client event for the Matrix
703 /// client-server API.
704 ///
705 /// This method transforms the event into the format expected by Matrix
706 /// clients, removing federation-only fields and adding client-specific
707 /// metadata.
708 ///
709 /// Returns a [`ClientEvent`] (boxed raw JSON) ready to be sent to a Matrix
710 /// client via sync, `/rooms/{roomId}/messages`, or other client API
711 /// endpoints.
712 ///
713 /// ## Transformations
714 ///
715 /// The conversion:
716 /// - Removes federation-only fields: `auth_events`, `prev_events`
717 /// - Always includes `event_id` (regardless of room version)
718 /// - Adds `unsigned` object with client context:
719 /// - `transaction_id`: Local transaction ID if this was sent by the client
720 /// - `age`: Event age in milliseconds (for sync responses)
721 /// - Removes `depth` field (not used in client API)
722 ///
723 /// ## Client Context
724 ///
725 /// The [`ClientContext`] parameter provides client-specific metadata:
726 /// - `transaction_id`: Set when the client sent this event, allowing them to correlate their
727 /// send with the resulting event
728 /// - `age`: Time since the event was created, useful for clients to show relative timestamps
729 ///
730 /// Both fields are optional - omit them when not applicable.
731 ///
732 /// ## Parameters
733 ///
734 /// - `ctx`: Client-specific context containing optional transaction ID and age.
735 #[must_use]
736 pub fn to_client_event(&self, ctx: ClientContext) -> ClientEvent {
737 let mut event = serde_json::Map::new();
738 let content_map = self.state.signed.contextualized.draft.content_to_map();
739 event.insert("content".to_owned(), serde_json::Value::Object(content_map));
740 event.insert(
741 "event_id".to_owned(),
742 self.state.signed.contextualized.event_id.to_string().into(),
743 );
744 event.insert(
745 "origin_server_ts".to_owned(),
746 serde_json::Value::Number(serde_json::Number::from(u64::from(
747 self.state.signed.contextualized.origin_server_ts.0,
748 ))),
749 );
750 event.insert(
751 "room_id".to_owned(),
752 serde_json::Value::String(self.state.signed.contextualized.room_id.to_string()),
753 );
754 event.insert(
755 "sender".to_owned(),
756 serde_json::Value::String((self.state.signed.contextualized.draft.sender).to_string()),
757 );
758 if let Some(state_key) = &self.state.signed.contextualized.draft.state_key {
759 event.insert("state_key".to_owned(), serde_json::Value::String(state_key.to_owned()));
760 }
761 event.insert(
762 "type".to_owned(),
763 serde_json::Value::String(
764 self.state.signed.contextualized.draft.timeline_event_type.to_string(),
765 ),
766 );
767
768 let unsigned = build_unsigned(ctx);
769 if let Some(unsigned) = unsigned {
770 event.insert("unsigned".to_owned(), unsigned);
771 }
772
773 raw_json(&serde_json::Value::Object(event))
774 }
775
776 /// Convert the verified event to a stripped state event for the
777 /// client-server API.
778 ///
779 /// Specifically, we are following the specification here: <https://spec.matrix.org/v1.18/client-server-api/#stripped-state>
780 ///
781 /// The event contains the fields `content`, `sender`, `state_key` and
782 /// `type`
783 ///
784 /// # Errors
785 ///
786 /// This function will return an error if it's called on a non-state event
787 pub fn to_stripped_state_event(
788 &self,
789 ) -> Result<Raw<AnyStrippedStateEvent>, EventIsNotStateEvent> {
790 let state_key =
791 self.state.signed.contextualized.draft.state_key.clone().ok_or(EventIsNotStateEvent)?;
792 let event = json!({
793 "content": self.state.signed.contextualized.draft.content,
794 "sender": self.state.signed.contextualized.draft.sender,
795 "state_key": state_key,
796 "type": self.state.signed.contextualized.draft.timeline_event_type,
797 });
798 Ok(Raw::from_json(raw_json(&event)))
799 }
800
801 /// Add an additional signature to this already verified event.
802 ///
803 /// ## Rationale
804 ///
805 /// This is used when joining a federated room. The joining server signs
806 /// the event, after which the resident server counter-signs it before
807 /// persisting and federating it.
808 ///
809 /// ## Errors
810 ///
811 /// Returns a [`SigningError`] if canonical JSON construction or signing
812 /// fails for any reason
813 pub fn sign(
814 self,
815 server_name: &ruma::ServerName,
816 signing_key: &Ed25519KeyPair,
817 ) -> Result<Self, SigningError> {
818 let room_version_rules = &self.state.signed.contextualized.room_version.rules();
819 let mut canonical = self.state.signed.build_event_object()?;
820 ruma::signatures::hash_and_sign_event(
821 server_name.as_str(),
822 signing_key,
823 &mut canonical,
824 &room_version_rules.redaction,
825 )
826 .map_err(|error| SigningError::SignatureCreation(error.to_string()))?;
827
828 let signatures = extract_signatures(&canonical)?;
829
830 Ok(Self {
831 state: Verified {
832 signed: Signed { signatures, ..self.state.signed },
833 metadata: self.state.metadata,
834 },
835 })
836 }
837}