events: introduce custom StateKey type for call member state events

This commit is contained in:
Timo 2024-09-12 08:28:06 +02:00 committed by GitHub
parent 1a138ed6c9
commit d92404d114
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 293 additions and 11 deletions

View File

@ -6,9 +6,11 @@
mod focus; mod focus;
mod member_data; mod member_data;
mod member_state_key;
pub use focus::*; pub use focus::*;
pub use member_data::*; pub use member_data::*;
pub use member_state_key::*;
use ruma_common::MilliSecondsSinceUnixEpoch; use ruma_common::MilliSecondsSinceUnixEpoch;
use ruma_macros::{EventContent, StringEnum}; use ruma_macros::{EventContent, StringEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -29,7 +31,7 @@ use crate::{
/// ///
/// This struct also exposes allows to call the methods from [`CallMemberEventContent`]. /// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)] #[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)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)] #[serde(untagged)]
pub enum CallMemberEventContent { pub enum CallMemberEventContent {
@ -177,7 +179,7 @@ impl RedactContent for CallMemberEventContent {
pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent; pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent { impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
type StateKey = String; type StateKey = CallMemberStateKey;
} }
/// The Redacted version of [`CallMemberEventContent`]. /// The Redacted version of [`CallMemberEventContent`].
@ -193,7 +195,7 @@ impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
} }
impl RedactedStateEventContent for RedactedCallMemberEventContent { impl RedactedStateEventContent for RedactedCallMemberEventContent {
type StateKey = String; type StateKey = CallMemberStateKey;
} }
/// Legacy content with an array of memberships. See also: [`CallMemberEventContent`] /// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
@ -231,8 +233,11 @@ mod tests {
use std::time::Duration; use std::time::Duration;
use assert_matches2::assert_matches; use assert_matches2::assert_matches;
use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId}; use ruma_common::{
use serde_json::{from_value as from_json_value, json}; 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::{ use super::{
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus}, focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
@ -467,8 +472,8 @@ mod tests {
); );
} }
fn deserialize_member_event_helper(state_key: &str) { fn member_event_json(state_key: &str) -> JsonValue {
let ev = json!({ json!({
"content":{ "content":{
"application": "m.call", "application": "m.call",
"call_id": "", "call_id": "",
@ -497,7 +502,11 @@ mod tests {
"prev_content": {}, "prev_content": {},
"prev_sender":"@user:example.org", "prev_sender":"@user:example.org",
} }
}); })
}
fn deserialize_member_event_helper(state_key: &str) {
let ev = member_event_json(state_key);
assert_matches!( assert_matches!(
from_json_value(ev), from_json_value(ev),
@ -507,7 +516,7 @@ mod tests {
let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap(); let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
let sender = OwnedUserId::try_from("@user:example.org").unwrap(); let sender = OwnedUserId::try_from("@user:example.org").unwrap();
let room_id = OwnedRoomId::try_from("!1234: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.event_id, event_id);
assert_eq!(member_event.sender, sender); assert_eq!(member_event.sender, sender);
assert_eq!(member_event.room_id, room_id); assert_eq!(member_event.room_id, room_id);
@ -555,12 +564,12 @@ mod tests {
#[test] #[test]
fn deserialize_member_event_with_scoped_state_key_prefixed() { 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] #[test]
fn deserialize_member_event_with_scoped_state_key_unprefixed() { 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) { fn timestamps() -> (TS, TS, TS) {
@ -632,4 +641,52 @@ mod tests {
vec![] as Vec<MembershipData<'_>> 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(_)
);
}
} }

View 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);
}
}