events: introduce custom StateKey type for call member state events
This commit is contained in:
		
							parent
							
								
									1a138ed6c9
								
							
						
					
					
						commit
						d92404d114
					
				| @ -6,9 +6,11 @@ | ||||
| 
 | ||||
| mod focus; | ||||
| mod member_data; | ||||
| mod member_state_key; | ||||
| 
 | ||||
| pub use focus::*; | ||||
| pub use member_data::*; | ||||
| pub use member_state_key::*; | ||||
| use ruma_common::MilliSecondsSinceUnixEpoch; | ||||
| use ruma_macros::{EventContent, StringEnum}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| @ -29,7 +31,7 @@ use crate::{ | ||||
| ///
 | ||||
| /// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
 | ||||
| #[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)] | ||||
| #[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = String, custom_redacted, custom_possibly_redacted)] | ||||
| #[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)] | ||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||
| #[serde(untagged)] | ||||
| pub enum CallMemberEventContent { | ||||
| @ -177,7 +179,7 @@ impl RedactContent for CallMemberEventContent { | ||||
| pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent; | ||||
| 
 | ||||
| impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent { | ||||
|     type StateKey = String; | ||||
|     type StateKey = CallMemberStateKey; | ||||
| } | ||||
| 
 | ||||
| /// The Redacted version of [`CallMemberEventContent`].
 | ||||
| @ -193,7 +195,7 @@ impl ruma_events::content::EventContent for RedactedCallMemberEventContent { | ||||
| } | ||||
| 
 | ||||
| impl RedactedStateEventContent for RedactedCallMemberEventContent { | ||||
|     type StateKey = String; | ||||
|     type StateKey = CallMemberStateKey; | ||||
| } | ||||
| 
 | ||||
| /// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
 | ||||
| @ -231,8 +233,11 @@ mod tests { | ||||
|     use std::time::Duration; | ||||
| 
 | ||||
|     use assert_matches2::assert_matches; | ||||
|     use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId}; | ||||
|     use serde_json::{from_value as from_json_value, json}; | ||||
|     use ruma_common::{ | ||||
|         device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, | ||||
|         OwnedUserId, | ||||
|     }; | ||||
|     use serde_json::{from_value as from_json_value, json, Value as JsonValue}; | ||||
| 
 | ||||
|     use super::{ | ||||
|         focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus}, | ||||
| @ -467,8 +472,8 @@ mod tests { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     fn deserialize_member_event_helper(state_key: &str) { | ||||
|         let ev = json!({ | ||||
|     fn member_event_json(state_key: &str) -> JsonValue { | ||||
|         json!({ | ||||
|             "content":{ | ||||
|                 "application": "m.call", | ||||
|                 "call_id": "", | ||||
| @ -497,7 +502,11 @@ mod tests { | ||||
|                 "prev_content": {}, | ||||
|                 "prev_sender":"@user:example.org", | ||||
|             } | ||||
|         }); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn deserialize_member_event_helper(state_key: &str) { | ||||
|         let ev = member_event_json(state_key); | ||||
| 
 | ||||
|         assert_matches!( | ||||
|             from_json_value(ev), | ||||
| @ -507,7 +516,7 @@ mod tests { | ||||
|         let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap(); | ||||
|         let sender = OwnedUserId::try_from("@user:example.org").unwrap(); | ||||
|         let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap(); | ||||
|         assert_eq!(member_event.state_key, state_key); | ||||
|         assert_eq!(member_event.state_key.as_ref(), state_key); | ||||
|         assert_eq!(member_event.event_id, event_id); | ||||
|         assert_eq!(member_event.sender, sender); | ||||
|         assert_eq!(member_event.room_id, room_id); | ||||
| @ -555,12 +564,12 @@ mod tests { | ||||
| 
 | ||||
|     #[test] | ||||
|     fn deserialize_member_event_with_scoped_state_key_prefixed() { | ||||
|         deserialize_member_event_helper("_@user:example.org:THIS_DEVICE"); | ||||
|         deserialize_member_event_helper("_@user:example.org_THIS_DEVICE"); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn deserialize_member_event_with_scoped_state_key_unprefixed() { | ||||
|         deserialize_member_event_helper("@user:example.org:THIS_DEVICE"); | ||||
|         deserialize_member_event_helper("@user:example.org_THIS_DEVICE"); | ||||
|     } | ||||
| 
 | ||||
|     fn timestamps() -> (TS, TS, TS) { | ||||
| @ -632,4 +641,52 @@ mod tests { | ||||
|             vec![] as Vec<MembershipData<'_>> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_parse_rtc_member_event_key() { | ||||
|         assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err()); | ||||
|         assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err()); | ||||
|         assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err()); | ||||
|         assert!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err() | ||||
|         ); | ||||
| 
 | ||||
|         let user_id = user_id!("@username:example.org").as_str(); | ||||
|         let device_id = device_id!("VALID_DEVICE_ID").as_str(); | ||||
| 
 | ||||
|         let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id)); | ||||
|         assert_matches!(parse_result, Ok(_)); | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))), | ||||
|             Ok(_) | ||||
|         ); | ||||
| 
 | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!( | ||||
|                 "{user_id}:invalid_suffix" | ||||
|             ))), | ||||
|             Err(_) | ||||
|         ); | ||||
| 
 | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))), | ||||
|             Err(_) | ||||
|         ); | ||||
| 
 | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))), | ||||
|             Ok(_) | ||||
|         ); | ||||
| 
 | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!( | ||||
|                 "_{user_id}:invalid_suffix" | ||||
|             ))), | ||||
|             Err(_) | ||||
|         ); | ||||
|         assert_matches!( | ||||
|             from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))), | ||||
|             Err(_) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										225
									
								
								crates/ruma-events/src/call/member/member_state_key.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								crates/ruma-events/src/call/member/member_state_key.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use ruma_common::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; | ||||
| use serde::{ | ||||
|     de::{self, Deserialize, Deserializer, Unexpected}, | ||||
|     Serialize, Serializer, | ||||
| }; | ||||
| /// A type that can be used as the `state_key` for call member state events.
 | ||||
| /// Those state keys can be a combination of UserId and DeviceId.
 | ||||
| #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] | ||||
| #[allow(clippy::exhaustive_structs)] | ||||
| pub struct CallMemberStateKey { | ||||
|     key: CallMemberStateKeyEnum, | ||||
|     raw: Box<str>, | ||||
| } | ||||
| 
 | ||||
| impl CallMemberStateKey { | ||||
|     /// Constructs a new CallMemberStateKey there are three possible formats:
 | ||||
|     /// - "_{UserId}_{DeviceId}" example: "_@test:user.org_DEVICE". `device_id`: Some`, `underscore:
 | ||||
|     ///   true`
 | ||||
|     /// - "{UserId}_{DeviceId}" example: "@test:user.org_DEVICE". `device_id`: Some`, `underscore:
 | ||||
|     ///   false`
 | ||||
|     /// - "{UserId}" example example: "@test:user.org". `device_id`: None`, underscore is ignored:
 | ||||
|     ///   `underscore: false|true`
 | ||||
|     ///
 | ||||
|     /// Dependent on the parameters the correct CallMemberStateKey will be constructed.
 | ||||
|     pub fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self { | ||||
|         CallMemberStateKeyEnum::new(user_id, device_id, underscore).into() | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the user id in this state key.
 | ||||
|     /// (This is a cheap operations. The id is already type checked on initialization. And does
 | ||||
|     /// only returns a reference to an existing OwnedUserId.)
 | ||||
|     pub fn user_id(&self) -> &UserId { | ||||
|         match &self.key { | ||||
|             CallMemberStateKeyEnum::UnderscoreUserDevice(u, _) => u, | ||||
|             CallMemberStateKeyEnum::UserDevice(u, _) => u, | ||||
|             CallMemberStateKeyEnum::User(u) => u, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the device id in this state key (if available)
 | ||||
|     /// (This is a cheap operations. The id is already type checked on initialization. And does
 | ||||
|     /// only returns a reference to an existing OwnedDeviceId.)
 | ||||
|     pub fn device_id(&self) -> Option<&DeviceId> { | ||||
|         match &self.key { | ||||
|             CallMemberStateKeyEnum::UnderscoreUserDevice(_, d) => Some(d), | ||||
|             CallMemberStateKeyEnum::UserDevice(_, d) => Some(d), | ||||
|             CallMemberStateKeyEnum::User(_) => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsRef<str> for CallMemberStateKey { | ||||
|     fn as_ref(&self) -> &str { | ||||
|         &self.raw | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<CallMemberStateKeyEnum> for CallMemberStateKey { | ||||
|     fn from(value: CallMemberStateKeyEnum) -> Self { | ||||
|         let raw = value.to_string().into(); | ||||
|         Self { key: value, raw } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for CallMemberStateKey { | ||||
|     type Err = KeyParseError; | ||||
| 
 | ||||
|     fn from_str(state_key: &str) -> Result<Self, Self::Err> { | ||||
|         // Intentionally do not use CallMemberStateKeyEnum.into since this would reconstruct the
 | ||||
|         // state key string.
 | ||||
|         Ok(Self { key: CallMemberStateKeyEnum::from_str(state_key)?, raw: state_key.into() }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'de> Deserialize<'de> for CallMemberStateKey { | ||||
|     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||
|     where | ||||
|         D: Deserializer<'de>, | ||||
|     { | ||||
|         let s = ruma_common::serde::deserialize_cow_str(deserializer)?; | ||||
|         Self::from_str(&s).map_err(|err| de::Error::invalid_value(Unexpected::Str(&s), &err)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Serialize for CallMemberStateKey { | ||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         S: Serializer, | ||||
|     { | ||||
|         serializer.serialize_str(self.as_ref()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// This enum represents all possible formats for a call member event state key.
 | ||||
| #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] | ||||
| enum CallMemberStateKeyEnum { | ||||
|     UnderscoreUserDevice(OwnedUserId, OwnedDeviceId), | ||||
|     UserDevice(OwnedUserId, OwnedDeviceId), | ||||
|     User(OwnedUserId), | ||||
| } | ||||
| 
 | ||||
| impl CallMemberStateKeyEnum { | ||||
|     fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self { | ||||
|         match (device_id, underscore) { | ||||
|             (Some(device_id), true) => { | ||||
|                 CallMemberStateKeyEnum::UnderscoreUserDevice(user_id, device_id) | ||||
|             } | ||||
|             (Some(device_id), false) => CallMemberStateKeyEnum::UserDevice(user_id, device_id), | ||||
|             (None, _) => CallMemberStateKeyEnum::User(user_id), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for CallMemberStateKeyEnum { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match &self { | ||||
|             CallMemberStateKeyEnum::UnderscoreUserDevice(u, d) => write!(f, "_{u}_{d}"), | ||||
|             CallMemberStateKeyEnum::UserDevice(u, d) => write!(f, "{u}_{d}"), | ||||
|             CallMemberStateKeyEnum::User(u) => f.write_str(u.as_str()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for CallMemberStateKeyEnum { | ||||
|     type Err = KeyParseError; | ||||
| 
 | ||||
|     fn from_str(state_key: &str) -> Result<Self, Self::Err> { | ||||
|         // Ignore leading underscore if present
 | ||||
|         // (used for avoiding auth rules on @-prefixed state keys)
 | ||||
|         let (state_key, underscore) = match state_key.strip_prefix('_') { | ||||
|             Some(s) => (s, true), | ||||
|             None => (state_key, false), | ||||
|         }; | ||||
| 
 | ||||
|         // Fail early if we cannot find the index of the ":"
 | ||||
|         let Some(colon_idx) = state_key.find(':') else { | ||||
|             return Err(KeyParseError::InvalidUser { | ||||
|                 user_id: state_key.to_owned(), | ||||
|                 error: ruma_common::IdParseError::MissingColon, | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         let (user_id, device_id) = match state_key[colon_idx + 1..].find('_') { | ||||
|             None => { | ||||
|                 return match UserId::parse(state_key) { | ||||
|                     Ok(user_id) => { | ||||
|                         if underscore { | ||||
|                             Err(KeyParseError::LeadingUnderscoreNoDevice) | ||||
|                         } else { | ||||
|                             Ok(CallMemberStateKeyEnum::new(user_id, None, underscore)) | ||||
|                         } | ||||
|                     } | ||||
|                     Err(err) => Err(KeyParseError::InvalidUser { | ||||
|                         error: err, | ||||
|                         user_id: state_key.to_owned(), | ||||
|                     }), | ||||
|                 } | ||||
|             } | ||||
|             Some(suffix_idx) => { | ||||
|                 (&state_key[..colon_idx + 1 + suffix_idx], &state_key[colon_idx + 2 + suffix_idx..]) | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         match (UserId::parse(user_id), OwnedDeviceId::from(device_id)) { | ||||
|             (Ok(user_id), device_id) => { | ||||
|                 if device_id.as_str().is_empty() { | ||||
|                     return Err(KeyParseError::EmptyDevice); | ||||
|                 }; | ||||
|                 Ok(CallMemberStateKeyEnum::new(user_id, Some(device_id), underscore)) | ||||
|             } | ||||
|             (Err(err), _) => { | ||||
|                 Err(KeyParseError::InvalidUser { user_id: user_id.to_owned(), error: err }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Error when trying to parse a call member state key.
 | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| #[non_exhaustive] | ||||
| pub enum KeyParseError { | ||||
|     /// The user part of the state key is invalid.
 | ||||
|     #[error("uses a malformatted UserId in the UserId defined section.")] | ||||
|     InvalidUser { | ||||
|         /// The user Id that the parser thinks it should have parsed.
 | ||||
|         user_id: String, | ||||
|         /// The user Id parse error why if failed to parse it.
 | ||||
|         error: ruma_common::IdParseError, | ||||
|     }, | ||||
|     /// Uses a leading underscore but no trailing device id. The part after the underscore is a
 | ||||
|     /// valid user id.
 | ||||
|     #[error("uses a leading underscore but no trailing device id. The part after the underscore is a valid user id.")] | ||||
|     LeadingUnderscoreNoDevice, | ||||
|     /// Uses an empty device id. (UserId with trailing underscore)
 | ||||
|     #[error("uses an empty device id. (UserId with trailing underscore)")] | ||||
|     EmptyDevice, | ||||
| } | ||||
| 
 | ||||
| impl de::Expected for KeyParseError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "correct call member event key format. The provided string, {})", self) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::str::FromStr; | ||||
| 
 | ||||
|     use crate::call::member::{member_state_key::CallMemberStateKeyEnum, CallMemberStateKey}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn convert_state_key_enum_to_state_key() { | ||||
|         let key = "_@user:domain.org_DEVICE"; | ||||
|         let state_key_enum = CallMemberStateKeyEnum::from_str(key).unwrap(); | ||||
|         // This generates state_key.raw from the enum
 | ||||
|         let state_key: CallMemberStateKey = state_key_enum.into(); | ||||
|         // This compares state_key.raw (generated) with key (original)
 | ||||
|         assert_eq!(state_key.as_ref(), key); | ||||
|         // Compare to the from string without `CallMemberStateKeyEnum` step.
 | ||||
|         let state_key_direct = CallMemberStateKey::from_str(state_key.as_ref()).unwrap(); | ||||
|         assert_eq!(state_key, state_key_direct); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user