Skip to main content

ruma_common/identifiers/
event_id.rs

1//! Matrix event identifiers.
2
3use ruma_macros::IdDst;
4
5use super::{IdParseError, ServerName};
6
7/// A Matrix [event ID].
8///
9/// An `EventId` is generated randomly or converted from a string slice, and can be converted back
10/// into a string as needed.
11///
12/// # Room versions
13///
14/// Matrix specifies multiple [room versions] and the format of event identifiers differ between
15/// them. The original format used by room versions 1 and 2 uses a short pseudorandom "localpart"
16/// followed by the hostname and port of the originating homeserver. Later room versions change
17/// event identifiers to be a hash of the event encoded with Base64. Some of the methods provided by
18/// `EventId` are only relevant to the original event format.
19///
20/// ```
21/// # use ruma_common::{server_name, EventId};
22/// // Room versions 1 and 2
23/// assert_eq!(<&EventId>::try_from("$h29iv0s8:example.com").unwrap(), "$h29iv0s8:example.com");
24///
25/// # #[cfg(feature = "rand")]
26/// # {
27/// let server_name = server_name!("example.com");
28/// let event_id = EventId::new_v1(server_name);
29/// assert_eq!(event_id.localpart().len(), 18);
30/// assert_eq!(event_id.server_name(), Some(server_name));
31/// # }
32///
33/// // Room version 3
34/// assert_eq!(
35///     <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
36///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
37/// );
38/// assert_eq!(
39///     EventId::new_v2_or_v3("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
40///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
41/// );
42///
43/// // Room version 4 and later
44/// assert_eq!(
45///     <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
46///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
47/// );
48/// assert_eq!(
49///     EventId::new_v2_or_v3("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
50///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
51/// );
52/// ```
53///
54/// [event ID]: https://spec.matrix.org/latest/appendices/#event-ids
55/// [room versions]: https://spec.matrix.org/latest/rooms/#complete-list-of-room-versions
56#[repr(transparent)]
57#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
58#[ruma_id(validate = ruma_identifiers_validation::event_id::validate)]
59pub struct EventId(str);
60
61impl EventId {
62    /// Attempts to generate an `OwnedEventId` for the given origin server with a localpart
63    /// consisting of 18 random ASCII characters.
64    ///
65    /// This generates an event ID matching the [`EventIdFormatVersion::V1`] variant of the
66    /// `event_id_format` field of [`RoomVersionRules`]. To construct an event ID matching the
67    /// [`EventIdFormatVersion::V2`] or [`EventIdFormatVersion::V3`] variants, use
68    /// [`EventId::new_v2_or_v3()`] instead.
69    ///
70    /// [`EventIdFormatVersion::V1`]: crate::room_version_rules::EventIdFormatVersion::V1
71    /// [`EventIdFormatVersion::V2`]: crate::room_version_rules::EventIdFormatVersion::V2
72    /// [`EventIdFormatVersion::V3`]: crate::room_version_rules::EventIdFormatVersion::V3
73    /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules
74    #[cfg(feature = "rand")]
75    #[allow(clippy::new_ret_no_self)]
76    pub fn new_v1(server_name: &ServerName) -> OwnedEventId {
77        OwnedEventId::from_string_unchecked(format!(
78            "${}:{server_name}",
79            super::generate_localpart(18)
80        ))
81    }
82
83    /// Construct an `OwnedEventId` using the reference hash of the event.
84    ///
85    /// This generates a room ID matching the [`EventIdFormatVersion::V2`] or
86    /// [`EventIdFormatVersion::V3`] variants of the `event_id_format` field of
87    /// [`RoomVersionRules`]. To construct an event ID matching the [`EventIdFormatVersion::V1`]
88    /// variant, use [`EventId::new_v1()`] instead.
89    ///
90    /// Returns an error if the given string contains a NUL byte or is too long.
91    ///
92    /// [`EventIdFormatVersion::V1`]: crate::room_version_rules::EventIdFormatVersion::V1
93    /// [`EventIdFormatVersion::V2`]: crate::room_version_rules::EventIdFormatVersion::V2
94    /// [`EventIdFormatVersion::V3`]: crate::room_version_rules::EventIdFormatVersion::V3
95    /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules
96    pub fn new_v2_or_v3(reference_hash: &str) -> Result<OwnedEventId, IdParseError> {
97        OwnedEventId::try_from(format!("${reference_hash}"))
98    }
99
100    /// Returns the event's unique ID.
101    ///
102    /// For the original event format as used by Matrix room versions 1 and 2, this is the
103    /// "localpart" that precedes the homeserver. For later formats, this is the entire ID without
104    /// the leading `$` sigil.
105    pub fn localpart(&self) -> &str {
106        let idx = self.colon_idx().unwrap_or_else(|| self.as_str().len());
107        &self.as_str()[1..idx]
108    }
109
110    /// Returns the server name of the event ID.
111    ///
112    /// Only applicable to events in the original format as used by Matrix room versions 1 and 2.
113    pub fn server_name(&self) -> Option<&ServerName> {
114        self.colon_idx().map(|idx| ServerName::from_borrowed_unchecked(&self.as_str()[idx + 1..]))
115    }
116
117    fn colon_idx(&self) -> Option<usize> {
118        self.as_str().find(':')
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::{EventId, OwnedEventId};
125    use crate::IdParseError;
126
127    #[test]
128    fn valid_original_event_id() {
129        assert_eq!(
130            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId."),
131            "$39hvsi03hlne:example.com"
132        );
133    }
134
135    #[test]
136    fn valid_base64_event_id() {
137        assert_eq!(
138            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
139                .expect("Failed to create EventId."),
140            "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
141        );
142    }
143
144    #[test]
145    fn valid_url_safe_base64_event_id() {
146        assert_eq!(
147            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
148                .expect("Failed to create EventId."),
149            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
150        );
151    }
152
153    #[cfg(feature = "rand")]
154    #[test]
155    fn generate_random_valid_event_id() {
156        use crate::server_name;
157
158        let server_name = server_name!("example.com");
159        let event_id = EventId::new_v1(server_name);
160        let id_str = event_id.as_str();
161
162        assert!(id_str.starts_with('$'));
163        assert_eq!(id_str.len(), 31);
164        assert_eq!(event_id.server_name(), Some(server_name));
165    }
166
167    #[test]
168    fn serialize_valid_original_event_id() {
169        assert_eq!(
170            serde_json::to_string(
171                <&EventId>::try_from("$39hvsi03hlne:example.com")
172                    .expect("Failed to create EventId.")
173            )
174            .expect("Failed to convert EventId to JSON."),
175            r#""$39hvsi03hlne:example.com""#
176        );
177    }
178
179    #[test]
180    fn serialize_valid_base64_event_id() {
181        assert_eq!(
182            serde_json::to_string(
183                <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
184                    .expect("Failed to create EventId.")
185            )
186            .expect("Failed to convert EventId to JSON."),
187            r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
188        );
189    }
190
191    #[test]
192    fn serialize_valid_url_safe_base64_event_id() {
193        assert_eq!(
194            serde_json::to_string(
195                <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
196                    .expect("Failed to create EventId.")
197            )
198            .expect("Failed to convert EventId to JSON."),
199            r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
200        );
201    }
202
203    #[test]
204    fn deserialize_valid_original_event_id() {
205        assert_eq!(
206            serde_json::from_str::<OwnedEventId>(r#""$39hvsi03hlne:example.com""#)
207                .expect("Failed to convert JSON to EventId"),
208            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
209        );
210    }
211
212    #[test]
213    fn deserialize_valid_base64_event_id() {
214        assert_eq!(
215            serde_json::from_str::<OwnedEventId>(
216                r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
217            )
218            .expect("Failed to convert JSON to EventId"),
219            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
220                .expect("Failed to create EventId.")
221        );
222    }
223
224    #[test]
225    fn deserialize_valid_url_safe_base64_event_id() {
226        assert_eq!(
227            serde_json::from_str::<OwnedEventId>(
228                r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
229            )
230            .expect("Failed to convert JSON to EventId"),
231            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
232                .expect("Failed to create EventId.")
233        );
234    }
235
236    #[test]
237    fn valid_original_event_id_with_explicit_standard_port() {
238        assert_eq!(
239            <&EventId>::try_from("$39hvsi03hlne:example.com:443")
240                .expect("Failed to create EventId."),
241            "$39hvsi03hlne:example.com:443"
242        );
243    }
244
245    #[test]
246    fn valid_original_event_id_with_non_standard_port() {
247        assert_eq!(
248            <&EventId>::try_from("$39hvsi03hlne:example.com:5000")
249                .expect("Failed to create EventId."),
250            "$39hvsi03hlne:example.com:5000"
251        );
252    }
253
254    #[test]
255    fn missing_original_event_id_sigil() {
256        assert_eq!(
257            <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(),
258            IdParseError::MissingLeadingSigil
259        );
260    }
261
262    #[test]
263    fn missing_base64_event_id_sigil() {
264        assert_eq!(
265            <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
266            IdParseError::MissingLeadingSigil
267        );
268    }
269
270    #[test]
271    fn missing_url_safe_base64_event_id_sigil() {
272        assert_eq!(
273            <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
274            IdParseError::MissingLeadingSigil
275        );
276    }
277
278    #[test]
279    fn invalid_event_id_host() {
280        assert_eq!(
281            <&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(),
282            IdParseError::InvalidServerName
283        );
284    }
285
286    #[test]
287    fn invalid_event_id_port() {
288        assert_eq!(
289            <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
290            IdParseError::InvalidServerName
291        );
292    }
293
294    #[test]
295    fn construct_v2_or_v3_event_id() {
296        assert_eq!(
297            EventId::new_v2_or_v3("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
298            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
299        );
300    }
301}