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