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 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(_)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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