ruma_events/call/member/
member_state_key.rs

1use std::str::FromStr;
2
3use ruma_common::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
4use serde::{
5    de::{self, Deserialize, Deserializer, Unexpected},
6    Serialize, Serializer,
7};
8/// A type that can be used as the `state_key` for call member state events.
9/// Those state keys can be a combination of UserId and DeviceId.
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
11#[allow(clippy::exhaustive_structs)]
12pub struct CallMemberStateKey {
13    key: CallMemberStateKeyEnum,
14    raw: Box<str>,
15}
16
17impl CallMemberStateKey {
18    /// Constructs a new CallMemberStateKey there are three possible formats:
19    /// - `_{UserId}_{DeviceId}` example: `_@test:user.org_DEVICE`. `device_id: Some`, `underscore:
20    ///   true`
21    /// - `{UserId}_{DeviceId}` example: `@test:user.org_DEVICE`. `device_id: Some`, `underscore:
22    ///   false`
23    /// - `{UserId}` example: `@test:user.org`. `device_id: None`, underscore is ignored:
24    ///   `underscore: false|true`
25    ///
26    /// Dependent on the parameters the correct CallMemberStateKey will be constructed.
27    pub fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self {
28        CallMemberStateKeyEnum::new(user_id, device_id, underscore).into()
29    }
30
31    /// Returns the user id in this state key.
32    /// (This is a cheap operations. The id is already type checked on initialization. And does
33    /// only returns a reference to an existing OwnedUserId.)
34    pub fn user_id(&self) -> &UserId {
35        match &self.key {
36            CallMemberStateKeyEnum::UnderscoreUserDevice(u, _) => u,
37            CallMemberStateKeyEnum::UserDevice(u, _) => u,
38            CallMemberStateKeyEnum::User(u) => u,
39        }
40    }
41
42    /// Returns the device id in this state key (if available)
43    /// (This is a cheap operations. The id is already type checked on initialization. And does
44    /// only returns a reference to an existing OwnedDeviceId.)
45    pub fn device_id(&self) -> Option<&DeviceId> {
46        match &self.key {
47            CallMemberStateKeyEnum::UnderscoreUserDevice(_, d) => Some(d),
48            CallMemberStateKeyEnum::UserDevice(_, d) => Some(d),
49            CallMemberStateKeyEnum::User(_) => None,
50        }
51    }
52}
53
54impl AsRef<str> for CallMemberStateKey {
55    fn as_ref(&self) -> &str {
56        &self.raw
57    }
58}
59
60impl From<CallMemberStateKeyEnum> for CallMemberStateKey {
61    fn from(value: CallMemberStateKeyEnum) -> Self {
62        let raw = value.to_string().into();
63        Self { key: value, raw }
64    }
65}
66
67impl FromStr for CallMemberStateKey {
68    type Err = KeyParseError;
69
70    fn from_str(state_key: &str) -> Result<Self, Self::Err> {
71        // Intentionally do not use CallMemberStateKeyEnum.into since this would reconstruct the
72        // state key string.
73        Ok(Self { key: CallMemberStateKeyEnum::from_str(state_key)?, raw: state_key.into() })
74    }
75}
76
77impl<'de> Deserialize<'de> for CallMemberStateKey {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: Deserializer<'de>,
81    {
82        let s = ruma_common::serde::deserialize_cow_str(deserializer)?;
83        Self::from_str(&s).map_err(|err| de::Error::invalid_value(Unexpected::Str(&s), &err))
84    }
85}
86
87impl Serialize for CallMemberStateKey {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        serializer.serialize_str(self.as_ref())
93    }
94}
95
96/// This enum represents all possible formats for a call member event state key.
97#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
98enum CallMemberStateKeyEnum {
99    UnderscoreUserDevice(OwnedUserId, OwnedDeviceId),
100    UserDevice(OwnedUserId, OwnedDeviceId),
101    User(OwnedUserId),
102}
103
104impl CallMemberStateKeyEnum {
105    fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self {
106        match (device_id, underscore) {
107            (Some(device_id), true) => {
108                CallMemberStateKeyEnum::UnderscoreUserDevice(user_id, device_id)
109            }
110            (Some(device_id), false) => CallMemberStateKeyEnum::UserDevice(user_id, device_id),
111            (None, _) => CallMemberStateKeyEnum::User(user_id),
112        }
113    }
114}
115
116impl std::fmt::Display for CallMemberStateKeyEnum {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match &self {
119            CallMemberStateKeyEnum::UnderscoreUserDevice(u, d) => write!(f, "_{u}_{d}"),
120            CallMemberStateKeyEnum::UserDevice(u, d) => write!(f, "{u}_{d}"),
121            CallMemberStateKeyEnum::User(u) => f.write_str(u.as_str()),
122        }
123    }
124}
125
126impl FromStr for CallMemberStateKeyEnum {
127    type Err = KeyParseError;
128
129    fn from_str(state_key: &str) -> Result<Self, Self::Err> {
130        // Ignore leading underscore if present
131        // (used for avoiding auth rules on @-prefixed state keys)
132        let (state_key, underscore) = match state_key.strip_prefix('_') {
133            Some(s) => (s, true),
134            None => (state_key, false),
135        };
136
137        // Fail early if we cannot find the index of the ":"
138        let Some(colon_idx) = state_key.find(':') else {
139            return Err(KeyParseError::InvalidUser {
140                user_id: state_key.to_owned(),
141                error: ruma_common::IdParseError::MissingColon,
142            });
143        };
144
145        let (user_id, device_id) = match state_key[colon_idx + 1..].find('_') {
146            None => {
147                return match UserId::parse(state_key) {
148                    Ok(user_id) => {
149                        if underscore {
150                            Err(KeyParseError::LeadingUnderscoreNoDevice)
151                        } else {
152                            Ok(CallMemberStateKeyEnum::new(user_id, None, underscore))
153                        }
154                    }
155                    Err(err) => Err(KeyParseError::InvalidUser {
156                        error: err,
157                        user_id: state_key.to_owned(),
158                    }),
159                }
160            }
161            Some(suffix_idx) => {
162                (&state_key[..colon_idx + 1 + suffix_idx], &state_key[colon_idx + 2 + suffix_idx..])
163            }
164        };
165
166        match (UserId::parse(user_id), OwnedDeviceId::from(device_id)) {
167            (Ok(user_id), device_id) => {
168                if device_id.as_str().is_empty() {
169                    return Err(KeyParseError::EmptyDevice);
170                };
171                Ok(CallMemberStateKeyEnum::new(user_id, Some(device_id), underscore))
172            }
173            (Err(err), _) => {
174                Err(KeyParseError::InvalidUser { user_id: user_id.to_owned(), error: err })
175            }
176        }
177    }
178}
179
180/// Error when trying to parse a call member state key.
181#[derive(Debug, thiserror::Error)]
182#[non_exhaustive]
183pub enum KeyParseError {
184    /// The user part of the state key is invalid.
185    #[error("uses a malformatted UserId in the UserId defined section.")]
186    InvalidUser {
187        /// The user Id that the parser thinks it should have parsed.
188        user_id: String,
189        /// The user Id parse error why if failed to parse it.
190        error: ruma_common::IdParseError,
191    },
192    /// Uses a leading underscore but no trailing device id. The part after the underscore is a
193    /// valid user id.
194    #[error("uses a leading underscore but no trailing device id. The part after the underscore is a valid user id.")]
195    LeadingUnderscoreNoDevice,
196    /// Uses an empty device id. (UserId with trailing underscore)
197    #[error("uses an empty device id. (UserId with trailing underscore)")]
198    EmptyDevice,
199}
200
201impl de::Expected for KeyParseError {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "correct call member event key format. The provided string, {})", self)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use std::str::FromStr;
210
211    use crate::call::member::{member_state_key::CallMemberStateKeyEnum, CallMemberStateKey};
212
213    #[test]
214    fn convert_state_key_enum_to_state_key() {
215        let key = "_@user:domain.org_DEVICE";
216        let state_key_enum = CallMemberStateKeyEnum::from_str(key).unwrap();
217        // This generates state_key.raw from the enum
218        let state_key: CallMemberStateKey = state_key_enum.into();
219        // This compares state_key.raw (generated) with key (original)
220        assert_eq!(state_key.as_ref(), key);
221        // Compare to the from string without `CallMemberStateKeyEnum` step.
222        let state_key_direct = CallMemberStateKey::from_str(state_key.as_ref()).unwrap();
223        assert_eq!(state_key, state_key_direct);
224    }
225}