Skip to main content

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}