Skip to main content

arminius_lib_events/
types.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Foundational type definitions and aliases used throughout the crate.
6//!
7//! This module provides the basic building stuff for event handling,
8//! including type aliases for common `ruma` types, event reference types,
9//! and metadata structures.
10
11#![allow(
12	clippy::allow_attributes,
13	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
14)]
15
16use std::{iter, str::FromStr};
17
18use ruma::{
19	EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, canonical_json,
20	room_version_rules::{EventsReferenceFormatVersion, RoomVersionRules},
21};
22use serde::{Deserialize, Deserializer, Serialize};
23
24use crate::{
25	conversions::serialize_hashed_event_ref,
26	errors::{UnknownRoomVersionError, UnknownRoomVersionStringError},
27};
28
29/// Matrix state key, part of the key of the state map
30pub type StateKey = String;
31
32/// Content hashes for a Matrix event.
33///
34/// The Matrix spec currently defines only one hash algorithm (`sha256`).
35/// Represented in PDUs as `{"sha256": "<base64-encoded-hash>"}`.
36#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub struct EventHash {
38	/// SHA-256 content hash, base64-encoded without padding.
39	sha256: String,
40}
41
42impl EventHash {
43	/// Creates a new [`EventHash`] with the given SHA-256 hash string.
44	#[must_use]
45	pub fn new(sha256: impl Into<String>) -> Self {
46		Self { sha256: sha256.into() }
47	}
48
49	/// Returns the SHA-256 content hash as a base64-encoded string.
50	#[must_use]
51	pub fn sha256(&self) -> &str {
52		&self.sha256
53	}
54
55	/// Serializes this hash to a [`serde_json::Value`].
56	///
57	/// This is infallible because the struct contains only a `String` field,
58	/// which `serde_json` can always represent.
59	#[must_use]
60	pub fn to_json_value(&self) -> serde_json::Value {
61		serde_json::json!({ "sha256": self.sha256 })
62	}
63
64	/// Serializes this hash to a [`canonical_json::CanonicalJsonValue`].
65	///
66	/// Wrapper around [`EventHash::to_json_value`], which does an infallible conversion to the
67	/// canonical version.
68	#[must_use]
69	pub fn to_canonical_json_value(&self) -> canonical_json::CanonicalJsonValue {
70		#[expect(
71			clippy::expect_used,
72			reason = "We know this will succeed, because we know what the hashes object we're constructing looks like"
73		)]
74		canonical_json::to_canonical_value(self.to_json_value()).expect("We know this will succeed, because we know what the hashes object we're constructing looks like")
75	}
76}
77
78/// An event reference with content hash (room versions 1-2).
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct HashedEventRef {
81	/// Event ID.
82	pub(crate) event_id: OwnedEventId,
83	/// Content hash of the referenced event.
84	pub(crate) hashes: EventHash,
85}
86
87impl HashedEventRef {
88	/// Create a new hashed event reference
89	#[must_use]
90	pub fn new(event_id: OwnedEventId, hashes: EventHash) -> Self {
91		Self { event_id, hashes }
92	}
93
94	/// Get the event ID
95	#[must_use]
96	pub fn event_id(&self) -> &EventId {
97		&self.event_id
98	}
99
100	/// Get the content hash
101	#[must_use]
102	pub const fn hashes(&self) -> &EventHash {
103		&self.hashes
104	}
105}
106
107/// Event references in a room-version-specific format.
108#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
109#[serde(untagged)]
110pub enum EventRefs {
111	/// V1/V2 format: event IDs with content hashes
112	/// `[["$id", {"sha256": "hash"}]]`.
113	Hashed(Vec<HashedEventRef>),
114	/// V3+ format: event IDs only `["$id"]`.
115	Plain(Vec<OwnedEventId>),
116	/// Empty list of references, so we do not care about the version
117	Empty,
118}
119
120impl From<&EventRefs> for Vec<OwnedEventId> {
121	fn from(value: &EventRefs) -> Self {
122		value.event_ids().map(ToOwned::to_owned).collect()
123	}
124}
125
126impl EventRefs {
127	/// Returns an iterator over the event IDs, regardless of format.
128	#[must_use]
129	pub fn event_ids(&self) -> Box<dyn Iterator<Item = &OwnedEventId> + Send + Sync + '_> {
130		match self {
131			Self::Empty => Box::new(iter::empty()),
132			Self::Hashed(references) => {
133				Box::new(references.iter().map(|reference| &reference.event_id))
134			}
135			Self::Plain(ids) => Box::new(ids.iter()),
136		}
137	}
138
139	/// A deserializer for EventRefs which return the empty variant when empty
140	pub(crate) fn pdu_deserialize<'de, D>(deserializer: D) -> Result<Self, D::Error>
141	where
142		D: Deserializer<'de>,
143	{
144		let event_refs = EventRefs::deserialize(deserializer)?;
145		if event_refs.is_empty() { Ok(EventRefs::Empty) } else { Ok(event_refs) }
146	}
147
148	/// Serialize event refs in the format expected of a PDU
149	pub(crate) fn pdu_serialize(&self) -> serde_json::Value {
150		match self {
151			Self::Empty => serde_json::Value::Array(Vec::new()),
152			EventRefs::Hashed(events) => {
153				serde_json::Value::Array(events.iter().map(serialize_hashed_event_ref).collect())
154			}
155			EventRefs::Plain(events) => serde_json::Value::Array(
156				events
157					.iter()
158					.map(|event_id| serde_json::Value::String(event_id.to_string()))
159					.collect(),
160			),
161		}
162	}
163
164	/// Returns the number of event references.
165	#[must_use]
166	pub const fn len(&self) -> usize {
167		match self {
168			Self::Empty => 0,
169			Self::Hashed(refs) => refs.len(),
170			Self::Plain(ids) => ids.len(),
171		}
172	}
173
174	/// Returns true if there are no event references.
175	#[must_use]
176	pub const fn is_empty(&self) -> bool {
177		self.len() == 0
178	}
179}
180
181impl From<Vec<OwnedEventId>> for EventRefs {
182	fn from(events: Vec<OwnedEventId>) -> Self {
183		Self::Plain(events)
184	}
185}
186
187/// Describes how much of a ruma event was actually verified during a signature
188/// verification
189#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Copy)]
190pub enum PartsVerified {
191	/// We have access to the original unredacted event, and could verify
192	/// everything
193	CompleteEvent,
194	/// We could only check the signature of the hash, and not the original
195	/// event because it has been redacted
196	OnlySignatures,
197}
198
199impl From<ruma::signatures::Verified> for PartsVerified {
200	fn from(value: ruma::signatures::Verified) -> Self {
201		match value {
202			ruma::signatures::Verified::All => PartsVerified::CompleteEvent,
203			ruma::signatures::Verified::Signatures => PartsVerified::OnlySignatures,
204		}
205	}
206}
207
208/// Metadata recorded when attempting to verify the signature of an event.
209#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
210pub struct VerificationMetadata {
211	/// Timestamp when verification was attempted.
212	pub attempted_at: MilliSecondsSinceUnixEpoch,
213	/// Signature verification outcome for the event.
214	pub signature_verification: PartsVerified,
215	/// Room version used to verify the event.
216	pub room_version: KnownRoomVersionId,
217	/// Whether the event was soft-failed.
218	///
219	/// A soft-failed event passed all signature, event ID, and auth checks
220	/// against its declared `auth_events` and `previous_state`, but failed
221	/// authorization against the current room state (spec Server-Server
222	/// #5.1.6). It must still be persisted and participate in state
223	/// resolution, if further events are received which reference it, but
224	/// should not be relayed to clients or added to leaf events.
225	///
226	/// See <https://spec.matrix.org/v1.17/server-server-api/#soft-failure> for more information.
227	pub soft_failed: bool,
228}
229
230#[cfg(any(test, feature = "test_utils"))]
231impl VerificationMetadata {
232	/// Construct an example `VerificationMetadata` value with the given
233	/// `room_version` for testing purposes.
234	#[must_use]
235	pub fn example(room_version: KnownRoomVersionId) -> Self {
236		Self {
237			attempted_at: MilliSecondsSinceUnixEpoch::now(),
238			signature_verification: PartsVerified::CompleteEvent,
239			room_version,
240			soft_failed: false,
241		}
242	}
243}
244
245/// An alternative to [`ruma::RoomVersionId`], including only types supported by
246/// `arminius-lib-rooms`
247///
248/// The original [`ruma::RoomVersionId`] allows expressing types which are
249/// unknown, and therefore have no [`RoomVersionRules`], an error case we are
250/// not interested in handling after an event is constructed.
251#[derive(Copy, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
252#[allow(
253	clippy::missing_docs_in_private_items,
254	missing_docs,
255	reason = "The name of the room version is documentation enough"
256)]
257#[derive(Default)]
258pub enum KnownRoomVersionId {
259	V1,
260	V2,
261	V3,
262	V4,
263	V5,
264	V6,
265	V7,
266	V8,
267	V9,
268	V10,
269	#[default]
270	V11,
271	V12,
272}
273
274/// The room versions which are completely supported in arminius-lib-events
275pub const SUPPORTED_ROOM_VERSIONS: [KnownRoomVersionId; 10] = [
276	KnownRoomVersionId::V3,
277	KnownRoomVersionId::V4,
278	KnownRoomVersionId::V5,
279	KnownRoomVersionId::V6,
280	KnownRoomVersionId::V7,
281	KnownRoomVersionId::V8,
282	KnownRoomVersionId::V9,
283	KnownRoomVersionId::V10,
284	KnownRoomVersionId::V11,
285	KnownRoomVersionId::V12,
286];
287
288impl std::fmt::Display for KnownRoomVersionId {
289	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290		let room_version_id: ruma::RoomVersionId = (*self).into();
291		room_version_id.fmt(f)
292	}
293}
294
295impl TryFrom<ruma::RoomVersionId> for KnownRoomVersionId {
296	type Error = UnknownRoomVersionError;
297
298	fn try_from(value: ruma::RoomVersionId) -> Result<Self, Self::Error> {
299		// Man am I happy I took the time to learn about Helix's multi-cursor
300		// features
301		match value {
302			ruma::RoomVersionId::V1 => Ok(KnownRoomVersionId::V1),
303			ruma::RoomVersionId::V2 => Ok(KnownRoomVersionId::V2),
304			ruma::RoomVersionId::V3 => Ok(KnownRoomVersionId::V3),
305			ruma::RoomVersionId::V4 => Ok(KnownRoomVersionId::V4),
306			ruma::RoomVersionId::V5 => Ok(KnownRoomVersionId::V5),
307			ruma::RoomVersionId::V6 => Ok(KnownRoomVersionId::V6),
308			ruma::RoomVersionId::V7 => Ok(KnownRoomVersionId::V7),
309			ruma::RoomVersionId::V8 => Ok(KnownRoomVersionId::V8),
310			ruma::RoomVersionId::V9 => Ok(KnownRoomVersionId::V9),
311			ruma::RoomVersionId::V10 => Ok(KnownRoomVersionId::V10),
312			ruma::RoomVersionId::V11 => Ok(KnownRoomVersionId::V11),
313			ruma::RoomVersionId::V12 => Ok(KnownRoomVersionId::V12),
314			_ => Err(UnknownRoomVersionError(value)),
315		}
316	}
317}
318
319impl From<KnownRoomVersionId> for ruma::RoomVersionId {
320	fn from(value: KnownRoomVersionId) -> Self {
321		match value {
322			KnownRoomVersionId::V1 => ruma::RoomVersionId::V1,
323			KnownRoomVersionId::V2 => ruma::RoomVersionId::V2,
324			KnownRoomVersionId::V3 => ruma::RoomVersionId::V3,
325			KnownRoomVersionId::V4 => ruma::RoomVersionId::V4,
326			KnownRoomVersionId::V5 => ruma::RoomVersionId::V5,
327			KnownRoomVersionId::V6 => ruma::RoomVersionId::V6,
328			KnownRoomVersionId::V7 => ruma::RoomVersionId::V7,
329			KnownRoomVersionId::V8 => ruma::RoomVersionId::V8,
330			KnownRoomVersionId::V9 => ruma::RoomVersionId::V9,
331			KnownRoomVersionId::V10 => ruma::RoomVersionId::V10,
332			KnownRoomVersionId::V11 => ruma::RoomVersionId::V11,
333			KnownRoomVersionId::V12 => ruma::RoomVersionId::V12,
334		}
335	}
336}
337
338impl FromStr for KnownRoomVersionId {
339	type Err = UnknownRoomVersionStringError;
340
341	fn from_str(value: &str) -> Result<Self, Self::Err> {
342		match value {
343			"1" => Ok(KnownRoomVersionId::V1),
344			"2" => Ok(KnownRoomVersionId::V2),
345			"3" => Ok(KnownRoomVersionId::V3),
346			"4" => Ok(KnownRoomVersionId::V4),
347			"5" => Ok(KnownRoomVersionId::V5),
348			"6" => Ok(KnownRoomVersionId::V6),
349			"7" => Ok(KnownRoomVersionId::V7),
350			"8" => Ok(KnownRoomVersionId::V8),
351			"9" => Ok(KnownRoomVersionId::V9),
352			"10" => Ok(KnownRoomVersionId::V10),
353			"11" => Ok(KnownRoomVersionId::V11),
354			"12" => Ok(KnownRoomVersionId::V12),
355			_ => Err(UnknownRoomVersionStringError(value.to_owned())),
356		}
357	}
358}
359
360impl KnownRoomVersionId {
361	/// Get the `RoomVersionRules` for this `KnownRoomVersionId`
362	#[must_use]
363	pub fn rules(&self) -> RoomVersionRules {
364		match self {
365			KnownRoomVersionId::V1 => RoomVersionRules::V1,
366			KnownRoomVersionId::V2 => RoomVersionRules::V2,
367			KnownRoomVersionId::V3 => RoomVersionRules::V3,
368			KnownRoomVersionId::V4 => RoomVersionRules::V4,
369			KnownRoomVersionId::V5 => RoomVersionRules::V5,
370			KnownRoomVersionId::V6 => RoomVersionRules::V6,
371			KnownRoomVersionId::V7 => RoomVersionRules::V7,
372			KnownRoomVersionId::V8 => RoomVersionRules::V8,
373			KnownRoomVersionId::V9 => RoomVersionRules::V9,
374			KnownRoomVersionId::V10 => RoomVersionRules::V10,
375			KnownRoomVersionId::V11 => RoomVersionRules::V11,
376			KnownRoomVersionId::V12 => RoomVersionRules::V12,
377		}
378	}
379
380	/// Returns the `KnownEventsReferenceFormatVersion` associated with the
381	/// event. Guaranteed to be the same as
382	/// `self.rules().events_reference_format`
383	#[must_use]
384	pub(crate) const fn events_reference_format(self) -> KnownEventsReferenceFormatVersion {
385		match self {
386			KnownRoomVersionId::V1 | KnownRoomVersionId::V2 => {
387				KnownEventsReferenceFormatVersion::V1
388			}
389			KnownRoomVersionId::V3
390			| KnownRoomVersionId::V4
391			| KnownRoomVersionId::V5
392			| KnownRoomVersionId::V6
393			| KnownRoomVersionId::V7
394			| KnownRoomVersionId::V8
395			| KnownRoomVersionId::V9
396			| KnownRoomVersionId::V10
397			| KnownRoomVersionId::V11
398			| KnownRoomVersionId::V12 => KnownEventsReferenceFormatVersion::V2,
399		}
400	}
401}
402
403/// An alternative to
404/// [`ruma::room_version_rules::EventsReferenceFormatVersion`], including only
405/// types supported by `arminius-lib-rooms`
406///
407/// The original is marked as non_exhaustive, something in which we are not
408/// interested
409#[derive(Copy, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
410#[allow(
411	clippy::missing_docs_in_private_items,
412	missing_docs,
413	reason = "The name of the events reference format version is enough"
414)]
415pub enum KnownEventsReferenceFormatVersion {
416	V1,
417	V2,
418}
419
420impl From<KnownEventsReferenceFormatVersion> for EventsReferenceFormatVersion {
421	fn from(val: KnownEventsReferenceFormatVersion) -> Self {
422		match val {
423			KnownEventsReferenceFormatVersion::V1 => EventsReferenceFormatVersion::V1,
424			KnownEventsReferenceFormatVersion::V2 => EventsReferenceFormatVersion::V2,
425		}
426	}
427}
428
429/// Client context used when converting to client events.
430#[derive(Debug, Clone, Default)]
431pub struct ClientContext {
432	/// Transaction ID if the event was sent by this user
433	pub transaction_id: Option<OwnedTransactionId>,
434	/// In the case of a state event, the previous content
435	pub prev_content: Option<serde_json::Value>,
436	/// Age of the event in milliseconds
437	pub age: Option<u64>,
438}
439
440/// PDU format version bucket for room version compatibility.
441#[derive(Debug, Clone, Copy)]
442#[non_exhaustive]
443pub enum PduFormat {
444	/// PDU format for room versions 1-2.
445	V1,
446	/// PDU format for room versions 3-5.
447	V3,
448	/// PDU format for room versions 6-11.
449	V6,
450	/// PDU format for room version 12+.
451	V12,
452}
453
454impl From<KnownRoomVersionId> for PduFormat {
455	fn from(value: KnownRoomVersionId) -> Self {
456		match value {
457			KnownRoomVersionId::V1 | KnownRoomVersionId::V2 => PduFormat::V1,
458			KnownRoomVersionId::V3 | KnownRoomVersionId::V4 | KnownRoomVersionId::V5 => {
459				PduFormat::V3
460			}
461			KnownRoomVersionId::V6
462			| KnownRoomVersionId::V7
463			| KnownRoomVersionId::V8
464			| KnownRoomVersionId::V9
465			| KnownRoomVersionId::V10
466			| KnownRoomVersionId::V11 => PduFormat::V6,
467			KnownRoomVersionId::V12 => PduFormat::V12,
468		}
469	}
470}
471
472/// Event format options derived from room version rules.
473#[derive(Debug, Clone, Copy)]
474pub struct EventFormatOptions {
475	/// Event reference format version.
476	pub events_reference_format: EventsReferenceFormatVersion,
477	/// Whether event_id field is required in the PDU.
478	pub require_event_id: bool,
479	/// Whether room_id is required in m.room.create events.
480	pub require_room_create_room_id: bool,
481	/// Whether `redacts` belongs in `content` (V11+) vs top-level (V1-V10).
482	///
483	/// In room versions 1-10, the `redacts` field is a top-level PDU field.
484	/// In room versions 11+, it is placed inside `content.redacts` instead.
485	/// See: <https://spec.matrix.org/v1.17/rooms/v11/#redactions>
486	pub content_field_redacts: bool,
487}
488
489impl EventFormatOptions {
490	/// Extract event format options from room version rules.
491	#[must_use]
492	pub const fn from_room_version(room_version: &RoomVersionRules) -> Self {
493		Self {
494			events_reference_format: room_version.events_reference_format,
495			require_event_id: room_version.event_format.require_event_id,
496			require_room_create_room_id: room_version.event_format.require_room_create_room_id,
497			content_field_redacts: room_version.redaction.content_field_redacts,
498		}
499	}
500}
501
502#[cfg(test)]
503mod tests;