ruma_events/call/member/
member_state_key.rs

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