events: Support new matrixRTC membership format
This commit is contained in:
parent
f60c79727a
commit
c218174369
@ -42,7 +42,7 @@ unstable-msc3927 = ["unstable-msc3551"]
|
|||||||
unstable-msc3954 = ["unstable-msc1767"]
|
unstable-msc3954 = ["unstable-msc1767"]
|
||||||
unstable-msc3955 = ["unstable-msc1767"]
|
unstable-msc3955 = ["unstable-msc1767"]
|
||||||
unstable-msc3956 = ["unstable-msc1767"]
|
unstable-msc3956 = ["unstable-msc1767"]
|
||||||
unstable-msc4075 = []
|
unstable-msc4075 = ["unstable-msc3401"]
|
||||||
unstable-pdu = []
|
unstable-pdu = []
|
||||||
|
|
||||||
# Allow some mandatory fields to be missing, defaulting them to an empty string
|
# Allow some mandatory fields to be missing, defaulting them to an empty string
|
||||||
|
@ -1,65 +1,118 @@
|
|||||||
//! Types for matrixRTC state events ([MSC3401]).
|
//! Types for MatrixRTC state events ([MSC3401]).
|
||||||
//!
|
//!
|
||||||
//! This implements a newer/updated version of MSC3401.
|
//! This implements a newer/updated version of MSC3401.
|
||||||
//!
|
//!
|
||||||
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
||||||
|
|
||||||
use std::time::Duration;
|
mod focus;
|
||||||
|
mod member_data;
|
||||||
|
|
||||||
use as_variant::as_variant;
|
pub use focus::*;
|
||||||
use ruma_common::{serde::StringEnum, MilliSecondsSinceUnixEpoch, OwnedUserId};
|
pub use member_data::*;
|
||||||
use ruma_macros::EventContent;
|
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId};
|
||||||
|
use ruma_macros::{EventContent, StringEnum};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::PrivOwnedStr;
|
use crate::{
|
||||||
|
PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent,
|
||||||
|
StateEventType,
|
||||||
|
};
|
||||||
|
|
||||||
/// The member state event for a matrixRTC session.
|
/// The member state event for a MatrixRTC session.
|
||||||
///
|
///
|
||||||
/// This is the object containing all the data related to a matrix users participation in a
|
/// This is the object containing all the data related to a Matrix users participation in a
|
||||||
/// matrixRTC session. It consists of memberships / sessions.
|
/// MatrixRTC session.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
///
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// This is a unit struct with the enum [`CallMemberEventContent`] because a Ruma state event cannot
|
||||||
|
/// be an enum and we need this to be an untagged enum for parsing purposes. (see
|
||||||
|
/// [`CallMemberEventContent`])
|
||||||
|
///
|
||||||
|
/// 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 = OwnedUserId, custom_redacted, custom_possibly_redacted)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId)]
|
#[serde(untagged)]
|
||||||
pub struct CallMemberEventContent {
|
pub enum CallMemberEventContent {
|
||||||
/// A list of all the memberships that user currently has in this room.
|
/// The legacy format for m.call.member events. (An array of memberships. The devices of one
|
||||||
///
|
/// user.)
|
||||||
/// There can be multiple ones in cases the user participates with multiple devices or there
|
LegacyContent(LegacyMembershipContent),
|
||||||
/// are multiple RTC applications running.
|
/// Normal membership events. One event per membership. Multiple state keys will
|
||||||
///
|
/// be used to describe multiple devices for one user.
|
||||||
/// e.g. a call and a spacial experience.
|
SessionContent(SessionMembershipData),
|
||||||
///
|
/// An empty content means this user has been in a rtc session but is not anymore.
|
||||||
/// Important: This includes expired memberships.
|
Empty(EmptyMembershipData),
|
||||||
/// To retrieve a list including only valid memberships,
|
|
||||||
/// see [`active_memberships`](CallMemberEventContent::active_memberships).
|
|
||||||
pub memberships: Vec<Membership>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallMemberEventContent {
|
impl CallMemberEventContent {
|
||||||
/// Creates a new `CallMemberEventContent`.
|
/// Creates a new [`CallMemberEventContent`] with [`LegacyMembershipData`].
|
||||||
pub fn new(memberships: Vec<Membership>) -> Self {
|
pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
|
||||||
Self { memberships }
|
Self::LegacyContent(LegacyMembershipContent {
|
||||||
|
memberships, //: memberships.into_iter().map(MembershipData::Legacy).collect(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`CallMemberEventContent`] with [`SessionMembershipData`].
|
||||||
|
pub fn new(
|
||||||
|
application: Application,
|
||||||
|
device_id: String,
|
||||||
|
focus_active: ActiveFocus,
|
||||||
|
foci_preferred: Vec<Focus>,
|
||||||
|
created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Self {
|
||||||
|
Self::SessionContent(SessionMembershipData {
|
||||||
|
application,
|
||||||
|
device_id,
|
||||||
|
focus_active,
|
||||||
|
foci_preferred,
|
||||||
|
created_ts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Empty [`CallMemberEventContent`] representing a left membership.
|
||||||
|
pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
|
||||||
|
Self::Empty(EmptyMembershipData { leave_reason })
|
||||||
|
}
|
||||||
/// All non expired memberships in this member event.
|
/// All non expired memberships in this member event.
|
||||||
///
|
///
|
||||||
/// In most cases you want tu use this method instead of the public memberships field.
|
/// In most cases you want to use this method instead of the public memberships field.
|
||||||
/// The memberships field will also include expired events.
|
/// The memberships field will also include expired events.
|
||||||
///
|
///
|
||||||
|
/// This copies all the memberships and converts them
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in case
|
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in the
|
||||||
/// the Membership does not contain `created_ts`. (`origin_server_ts` will be ignored if
|
/// Membership does not contain [`LegacyMembershipData::created_ts`]. (`origin_server_ts` will
|
||||||
/// `created_ts` is `Some`)
|
/// be ignored if [`LegacyMembershipData::created_ts`] is `Some`)
|
||||||
pub fn active_memberships(
|
pub fn active_memberships(
|
||||||
&self,
|
&self,
|
||||||
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
) -> Vec<&Membership> {
|
) -> Vec<MembershipData<'_>> {
|
||||||
self.memberships.iter().filter(|m| !m.is_expired(origin_server_ts)).collect()
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.active_memberships(origin_server_ts)
|
||||||
|
}
|
||||||
|
CallMemberEventContent::SessionContent(content) => {
|
||||||
|
[content].map(MembershipData::Session).to_vec()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::Empty(_) => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the `created_ts` of each [Membership] in this event.
|
/// All the memberships for this event. Can only contain multiple elements in the case of legacy
|
||||||
|
/// `m.call.member` state events.
|
||||||
|
pub fn memberships(&self) -> Vec<MembershipData<'_>> {
|
||||||
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.memberships.iter().map(MembershipData::Legacy).collect()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::SessionContent(content) => {
|
||||||
|
[content].map(MembershipData::Session).to_vec()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::Empty(_) => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `created_ts` of each [`MembershipData::Legacy`] in this event.
|
||||||
///
|
///
|
||||||
/// Each call member event contains the `origin_server_ts` and `content.create_ts`.
|
/// Each call member event contains the `origin_server_ts` and `content.create_ts`.
|
||||||
/// `content.create_ts` is undefined for the initial event of a session (because the
|
/// `content.create_ts` is undefined for the initial event of a session (because the
|
||||||
@ -68,257 +121,130 @@ impl CallMemberEventContent {
|
|||||||
/// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
|
/// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
|
||||||
/// expired)
|
/// expired)
|
||||||
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
|
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
|
||||||
self.memberships.iter_mut().for_each(|m| {
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
|
||||||
m.created_ts.get_or_insert(origin_server_ts);
|
m.created_ts.get_or_insert(origin_server_ts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
CallMemberEventContent::SessionContent(m) => {
|
||||||
|
m.created_ts.get_or_insert(origin_server_ts);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A membership describes one of the sessions this user currently partakes.
|
/// This describes the CallMember event if the user is not part of the current session.
|
||||||
///
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
||||||
/// The application defines the type of the session.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
pub struct Membership {
|
pub struct EmptyMembershipData {
|
||||||
/// The type of the matrixRTC session the membership belongs to.
|
/// An empty call member state event can optionally contain a leave reason.
|
||||||
///
|
/// If it is `None` the user has left the call ordinarily. (Intentional hangup)
|
||||||
/// e.g. call, spacial, document...
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub application: Application,
|
|
||||||
|
|
||||||
/// The device id of this membership.
|
|
||||||
///
|
|
||||||
/// The same user can join with their phone/computer.
|
|
||||||
pub device_id: String,
|
|
||||||
|
|
||||||
/// The duration in milliseconds relative to the time this membership joined
|
|
||||||
/// during which the membership is valid.
|
|
||||||
///
|
|
||||||
/// The time a member has joined is defined as:
|
|
||||||
/// `MIN(content.created_ts, event.origin_server_ts)`
|
|
||||||
#[serde(with = "ruma_common::serde::duration::ms")]
|
|
||||||
pub expires: Duration,
|
|
||||||
|
|
||||||
/// Stores a copy of the `origin_server_ts` of the initial session event.
|
|
||||||
///
|
|
||||||
/// If the membership is updated this field will be used to track to
|
|
||||||
/// original `origin_server_ts`.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
pub leave_reason: Option<LeaveReason>,
|
||||||
|
|
||||||
/// A list of the foci in use for this membership.
|
|
||||||
pub foci_active: Vec<Focus>,
|
|
||||||
|
|
||||||
/// The id of the membership.
|
|
||||||
///
|
|
||||||
/// This is required to guarantee uniqueness of the event.
|
|
||||||
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
|
||||||
/// 200.
|
|
||||||
#[serde(rename = "membershipID")]
|
|
||||||
pub membership_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Membership {
|
/// This is the optional value for an empty membership event content:
|
||||||
/// The application of the membership is "m.call" and the scope is "m.room".
|
/// [`CallMemberEventContent::Empty`]. It is used when the user disconnected and a Future ([MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140))
|
||||||
pub fn is_room_call(&self) -> bool {
|
/// was used to update the membership after the client was not reachable anymore.
|
||||||
as_variant!(&self.application, Application::Call)
|
|
||||||
.is_some_and(|call| call.scope == CallScope::Room)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The application of the membership is "m.call".
|
|
||||||
pub fn is_call(&self) -> bool {
|
|
||||||
as_variant!(&self.application, Application::Call).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the event is expired.
|
|
||||||
///
|
|
||||||
/// Defaults to using `created_ts` of the `Membership`.
|
|
||||||
/// If no `origin_server_ts` is provided and the event does not contain `created_ts`
|
|
||||||
/// the event will be considered as not expired.
|
|
||||||
/// In this case, a warning will be logged.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `origin_server_ts` - a fallback if `created_ts` is not present
|
|
||||||
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
|
||||||
let ev_created_ts = self.created_ts.or(origin_server_ts);
|
|
||||||
|
|
||||||
if let Some(ev_created_ts) = ev_created_ts {
|
|
||||||
let now = MilliSecondsSinceUnixEpoch::now().to_system_time();
|
|
||||||
let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires);
|
|
||||||
now > expire_ts
|
|
||||||
} else {
|
|
||||||
// This should not be reached since we only allow events that have copied over
|
|
||||||
// the origin server ts. `set_created_ts_if_none`
|
|
||||||
warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\
|
|
||||||
It is treated as a non expired event but this might be wrong.");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initial set of fields of [`Membership`].
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(clippy::exhaustive_structs)]
|
|
||||||
pub struct MembershipInit {
|
|
||||||
/// The type of the matrixRTC session the membership belongs to.
|
|
||||||
///
|
|
||||||
/// e.g. call, spacial, document...
|
|
||||||
pub application: Application,
|
|
||||||
|
|
||||||
/// The device id of this membership.
|
|
||||||
///
|
|
||||||
/// The same user can join with their phone/computer.
|
|
||||||
pub device_id: String,
|
|
||||||
|
|
||||||
/// The duration in milliseconds relative to the time this membership joined
|
|
||||||
/// during which the membership is valid.
|
|
||||||
///
|
|
||||||
/// The time a member has joined is defined as:
|
|
||||||
/// `MIN(content.created_ts, event.origin_server_ts)`
|
|
||||||
pub expires: Duration,
|
|
||||||
|
|
||||||
/// A list of the focuses (foci) in use for this membership.
|
|
||||||
pub foci_active: Vec<Focus>,
|
|
||||||
|
|
||||||
/// The id of the membership.
|
|
||||||
///
|
|
||||||
/// This is required to guarantee uniqueness of the event.
|
|
||||||
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
|
||||||
/// 200.
|
|
||||||
pub membership_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MembershipInit> for Membership {
|
|
||||||
fn from(init: MembershipInit) -> Self {
|
|
||||||
let MembershipInit { application, device_id, expires, foci_active, membership_id } = init;
|
|
||||||
Self { application, device_id, expires, created_ts: None, foci_active, membership_id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Description of the SFU/Focus a membership can be connected to.
|
|
||||||
///
|
|
||||||
/// A focus can be any server powering the matrixRTC session (SFU,
|
|
||||||
/// MCU). It serves as a node to redistribute RTC streams.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
pub enum Focus {
|
|
||||||
/// Livekit is one possible type of SFU/Focus that can be used for a matrixRTC session.
|
|
||||||
Livekit(LivekitFocus),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The fields to describe livekit as an `active_foci`.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
||||||
pub struct LivekitFocus {
|
|
||||||
/// The alias where the livekit sessions can be reached.
|
|
||||||
#[serde(rename = "livekit_alias")]
|
|
||||||
pub alias: String,
|
|
||||||
|
|
||||||
/// The url of the jwt server for the livekit instance.
|
|
||||||
#[serde(rename = "livekit_service_url")]
|
|
||||||
pub service_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LivekitFocus {
|
|
||||||
/// Initialize a [`LivekitFocus`].
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `alias` - The alias where the livekit sessions can be reached.
|
|
||||||
/// * `service_url` - The url of the jwt server for the livekit instance.
|
|
||||||
pub fn new(alias: String, service_url: String) -> Self {
|
|
||||||
Self { alias, service_url }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type of the matrixRTC session.
|
|
||||||
///
|
|
||||||
/// This is not the application/client used by the user but the
|
|
||||||
/// type of matrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
|
|
||||||
/// possible applications.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
||||||
#[serde(tag = "application")]
|
|
||||||
pub enum Application {
|
|
||||||
#[serde(rename = "m.call")]
|
|
||||||
/// A VoIP call.
|
|
||||||
Call(CallApplicationContent),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call specific parameters membership parameters.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
||||||
pub struct CallApplicationContent {
|
|
||||||
/// An identifier for calls.
|
|
||||||
///
|
|
||||||
/// All members using the same `call_id` will end up in the same call.
|
|
||||||
///
|
|
||||||
/// Does not need to be a uuid.
|
|
||||||
///
|
|
||||||
/// `""` is used for room scoped calls.
|
|
||||||
pub call_id: String,
|
|
||||||
|
|
||||||
/// Who owns/joins/controls (can modify) the call.
|
|
||||||
pub scope: CallScope,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CallApplicationContent {
|
|
||||||
/// Initialize a [`CallApplicationContent`].
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
|
|
||||||
/// the same call. Does not need to be a uuid. `""` is used for room scoped calls.
|
|
||||||
/// * `scope` - Who owns/joins/controls (can modify) the call.
|
|
||||||
pub fn new(call_id: String, scope: CallScope) -> Self {
|
|
||||||
Self { call_id, scope }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The call scope defines different call ownership models.
|
|
||||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
|
||||||
#[derive(Clone, PartialEq, StringEnum)]
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_enum(rename_all = "m.snake_case")]
|
#[ruma_enum(rename_all = "m.snake_case")]
|
||||||
pub enum CallScope {
|
pub enum LeaveReason {
|
||||||
/// A call which every user of a room can join and create.
|
/// The user left the call by losing network connection or closing
|
||||||
///
|
/// the client before it was able to send the leave event.
|
||||||
/// There is no particular name associated with it.
|
LostConnection,
|
||||||
///
|
|
||||||
/// There can only be one per room.
|
|
||||||
Room,
|
|
||||||
|
|
||||||
/// A user call is owned by a user.
|
|
||||||
///
|
|
||||||
/// Each user can create one there can be multiple per room. They are started and ended by the
|
|
||||||
/// owning user.
|
|
||||||
User,
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
_Custom(PrivOwnedStr),
|
_Custom(PrivOwnedStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RedactContent for CallMemberEventContent {
|
||||||
|
type Redacted = RedactedCallMemberEventContent;
|
||||||
|
|
||||||
|
fn redact(self, _version: &ruma_common::RoomVersionId) -> Self::Redacted {
|
||||||
|
RedactedCallMemberEventContent {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PossiblyRedacted version of [`CallMemberEventContent`].
|
||||||
|
///
|
||||||
|
/// Since [`CallMemberEventContent`] has the [`CallMemberEventContent::Empty`] state it already is
|
||||||
|
/// compatible with the redacted version of the state event content.
|
||||||
|
pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
|
||||||
|
|
||||||
|
impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
|
||||||
|
type StateKey = OwnedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Redacted version of [`CallMemberEventContent`].
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
pub struct RedactedCallMemberEventContent {}
|
||||||
|
|
||||||
|
impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
|
||||||
|
type EventType = StateEventType;
|
||||||
|
fn event_type(&self) -> Self::EventType {
|
||||||
|
StateEventType::CallMember
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedactedStateEventContent for RedactedCallMemberEventContent {
|
||||||
|
type StateKey = OwnedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LegacyMembershipContent {
|
||||||
|
/// A list of all the memberships that user currently has in this room.
|
||||||
|
///
|
||||||
|
/// There can be multiple ones in case the user participates with multiple devices or there
|
||||||
|
/// are multiple RTC applications running.
|
||||||
|
///
|
||||||
|
/// e.g. a call and a spacial experience.
|
||||||
|
///
|
||||||
|
/// Important: This includes expired memberships.
|
||||||
|
/// To retrieve a list including only valid memberships,
|
||||||
|
/// see [`active_memberships`](CallMemberEventContent::active_memberships).
|
||||||
|
memberships: Vec<LegacyMembershipData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyMembershipContent {
|
||||||
|
fn active_memberships(
|
||||||
|
&self,
|
||||||
|
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Vec<MembershipData<'_>> {
|
||||||
|
self.memberships
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !m.is_expired(origin_server_ts))
|
||||||
|
.map(MembershipData::Legacy)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use ruma_common::MilliSecondsSinceUnixEpoch as TS;
|
use assert_matches2::assert_matches;
|
||||||
use serde_json::json;
|
use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId};
|
||||||
|
use serde_json::{from_value as from_json_value, json};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Application, CallApplicationContent, CallMemberEventContent, CallScope, Focus,
|
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
|
||||||
LivekitFocus, Membership,
|
member_data::{
|
||||||
|
Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
|
||||||
|
},
|
||||||
|
CallMemberEventContent,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
|
||||||
|
AnyStateEvent, StateEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn create_call_member_event_content() -> CallMemberEventContent {
|
fn create_call_member_legacy_event_content() -> CallMemberEventContent {
|
||||||
CallMemberEventContent::new(vec![Membership {
|
CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "123456".to_owned(),
|
call_id: "123456".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -334,8 +260,60 @@ mod tests {
|
|||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_call_member_event_content() -> CallMemberEventContent {
|
||||||
|
CallMemberEventContent::new(
|
||||||
|
Application::Call(CallApplicationContent {
|
||||||
|
call_id: "123456".to_owned(),
|
||||||
|
scope: CallScope::Room,
|
||||||
|
}),
|
||||||
|
"ABCDE".to_owned(),
|
||||||
|
ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
vec![Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "1".to_owned(),
|
||||||
|
service_url: "https://livekit.com".to_owned(),
|
||||||
|
})],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_call_member_event_content() {
|
fn serialize_call_member_event_content() {
|
||||||
|
let call_member_event = &json!({
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "123456",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "ABCDE",
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "1",
|
||||||
|
"livekit_service_url": "https://livekit.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"focus_active":{
|
||||||
|
"type":"livekit",
|
||||||
|
"focus_select":"oldest_membership"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
call_member_event,
|
||||||
|
&serde_json::to_value(create_call_member_event_content()).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty_call_member_event = &json!({});
|
||||||
|
assert_eq!(
|
||||||
|
empty_call_member_event,
|
||||||
|
&serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
|
||||||
|
leave_reason: None
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_legacy_call_member_event_content() {
|
||||||
let call_member_event = &json!({
|
let call_member_event = &json!({
|
||||||
"memberships": [
|
"memberships": [
|
||||||
{
|
{
|
||||||
@ -358,14 +336,62 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
call_member_event,
|
call_member_event,
|
||||||
&serde_json::to_value(create_call_member_event_content()).unwrap()
|
&serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deserialize_call_member_event_content() {
|
||||||
|
let call_member_ev = CallMemberEventContent::new(
|
||||||
|
Application::Call(CallApplicationContent {
|
||||||
|
call_id: "123456".to_owned(),
|
||||||
|
scope: CallScope::Room,
|
||||||
|
}),
|
||||||
|
"THIS_DEVICE".to_owned(),
|
||||||
|
ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
vec![Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "room1".to_owned(),
|
||||||
|
service_url: "https://livekit1.com".to_owned(),
|
||||||
|
})],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let call_member_ev_json = json!({
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "123456",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "THIS_DEVICE",
|
||||||
|
"focus_active":{
|
||||||
|
"type": "livekit",
|
||||||
|
"focus_select": "oldest_membership"
|
||||||
|
},
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "room1",
|
||||||
|
"livekit_service_url": "https://livekit1.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let ev_content: CallMemberEventContent =
|
||||||
|
serde_json::from_value(call_member_ev_json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&ev_content).unwrap(),
|
||||||
|
serde_json::to_string(&call_member_ev).unwrap()
|
||||||
|
);
|
||||||
|
let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&json!({})).unwrap(),
|
||||||
|
serde_json::to_string(&empty).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_call_member_event_content() {
|
fn deserialize_legacy_call_member_event_content() {
|
||||||
let call_member_ev: CallMemberEventContent = CallMemberEventContent::new(vec![
|
let call_member_ev = CallMemberEventContent::new_legacy(vec![
|
||||||
Membership {
|
LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "123456".to_owned(),
|
call_id: "123456".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -379,7 +405,7 @@ mod tests {
|
|||||||
membership_id: "0".to_owned(),
|
membership_id: "0".to_owned(),
|
||||||
created_ts: None,
|
created_ts: None,
|
||||||
},
|
},
|
||||||
Membership {
|
LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "".to_owned(),
|
call_id: "".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -432,7 +458,85 @@ mod tests {
|
|||||||
|
|
||||||
let ev_content: CallMemberEventContent =
|
let ev_content: CallMemberEventContent =
|
||||||
serde_json::from_value(call_member_ev_json).unwrap();
|
serde_json::from_value(call_member_ev_json).unwrap();
|
||||||
assert_eq!(ev_content, call_member_ev);
|
assert_eq!(
|
||||||
|
serde_json::to_string(&ev_content).unwrap(),
|
||||||
|
serde_json::to_string(&call_member_ev).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deserialize_member_event() {
|
||||||
|
let ev = json!({
|
||||||
|
"content":{
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "THIS_DEVICE",
|
||||||
|
"focus_active":{
|
||||||
|
"type": "livekit",
|
||||||
|
"focus_select": "oldest_membership"
|
||||||
|
},
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "room1",
|
||||||
|
"livekit_service_url": "https://livekit1.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"type": "m.call.member",
|
||||||
|
"origin_server_ts": 111,
|
||||||
|
"event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
|
||||||
|
"room_id": "!1234:example.org",
|
||||||
|
"sender": "@user:example.org",
|
||||||
|
"state_key":"@user:example.org",
|
||||||
|
"unsigned":{
|
||||||
|
"age":10,
|
||||||
|
"prev_content": {},
|
||||||
|
"prev_sender":"@user:example.org",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
from_json_value(ev),
|
||||||
|
Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
|
||||||
|
);
|
||||||
|
|
||||||
|
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, sender);
|
||||||
|
assert_eq!(member_event.event_id, event_id);
|
||||||
|
assert_eq!(member_event.sender, sender);
|
||||||
|
assert_eq!(member_event.room_id, room_id);
|
||||||
|
assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
|
||||||
|
assert_eq!(
|
||||||
|
member_event.content,
|
||||||
|
CallMemberEventContent::SessionContent(SessionMembershipData {
|
||||||
|
application: Application::Call(CallApplicationContent {
|
||||||
|
call_id: "".to_owned(),
|
||||||
|
scope: CallScope::Room
|
||||||
|
}),
|
||||||
|
device_id: "THIS_DEVICE".to_owned(),
|
||||||
|
foci_preferred: [Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "room1".to_owned(),
|
||||||
|
service_url: "https://livekit1.com".to_owned()
|
||||||
|
})]
|
||||||
|
.to_vec(),
|
||||||
|
focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership
|
||||||
|
}),
|
||||||
|
created_ts: None
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
|
||||||
|
assert_eq!(
|
||||||
|
CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
|
||||||
|
member_event.unsigned.prev_content.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert_eq!(, StateUnsigned { age: 10, transaction_id: None, prev_content:
|
||||||
|
// CallMemberEventContent::Empty { leave_reason: None }, relations: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timestamps() -> (TS, TS, TS) {
|
fn timestamps() -> (TS, TS, TS) {
|
||||||
@ -449,44 +553,59 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn membership_do_expire() {
|
fn memberships_do_expire() {
|
||||||
let content = create_call_member_event_content();
|
let content_legacy = create_call_member_legacy_event_content();
|
||||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content.active_memberships(Some(one_second_ago)),
|
content_legacy.active_memberships(Some(one_second_ago)),
|
||||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
content_legacy.memberships()
|
||||||
);
|
);
|
||||||
|
assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content.active_memberships(Some(now)),
|
content_legacy.active_memberships(Some(two_hours_ago)),
|
||||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
(vec![] as Vec<MembershipData<'_>>)
|
||||||
|
);
|
||||||
|
// session do never expire
|
||||||
|
let content_session = create_call_member_event_content();
|
||||||
|
assert_eq!(
|
||||||
|
content_session.active_memberships(Some(one_second_ago)),
|
||||||
|
content_session.memberships()
|
||||||
|
);
|
||||||
|
assert_eq!(content_session.active_memberships(Some(now)), content_session.memberships());
|
||||||
|
assert_eq!(
|
||||||
|
content_session.active_memberships(Some(two_hours_ago)),
|
||||||
|
content_session.memberships()
|
||||||
);
|
);
|
||||||
assert_eq!(content.active_memberships(Some(two_hours_ago)), vec![] as Vec<&Membership>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_created_ts() {
|
fn set_created_ts() {
|
||||||
let mut content_now = create_call_member_event_content();
|
let mut content_now = create_call_member_legacy_event_content();
|
||||||
let mut content_two_hours_ago = create_call_member_event_content();
|
let mut content_two_hours_ago = create_call_member_legacy_event_content();
|
||||||
let mut content_one_second_ago = create_call_member_event_content();
|
let mut content_one_second_ago = create_call_member_legacy_event_content();
|
||||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||||
|
|
||||||
content_now.set_created_ts_if_none(now);
|
content_now.set_created_ts_if_none(now);
|
||||||
content_one_second_ago.set_created_ts_if_none(one_second_ago);
|
content_one_second_ago.set_created_ts_if_none(one_second_ago);
|
||||||
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
|
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
|
||||||
assert_eq!(
|
assert_eq!(content_now.active_memberships(None), content_now.memberships());
|
||||||
content_now.active_memberships(None),
|
|
||||||
content_now.memberships.iter().collect::<Vec<&Membership>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>);
|
assert_eq!(
|
||||||
|
content_two_hours_ago.active_memberships(None),
|
||||||
|
vec![] as Vec<MembershipData<'_>>
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_one_second_ago.active_memberships(None),
|
content_one_second_ago.active_memberships(None),
|
||||||
content_one_second_ago.memberships.iter().collect::<Vec<&Membership>>()
|
content_one_second_ago.memberships()
|
||||||
);
|
);
|
||||||
|
|
||||||
// created_ts should not be overwritten.
|
// created_ts should not be overwritten.
|
||||||
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
|
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
|
||||||
// There still should be no active membership.
|
// There still should be no active membership.
|
||||||
assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>);
|
assert_eq!(
|
||||||
|
content_two_hours_ago.active_memberships(None),
|
||||||
|
vec![] as Vec<MembershipData<'_>>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
crates/ruma-events/src/call/member/focus.rs
Normal file
88
crates/ruma-events/src/call/member/focus.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//! Types for MatrixRTC Focus/SFU configurations.
|
||||||
|
|
||||||
|
use ruma_macros::StringEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
|
/// Description of the SFU/Focus a membership can be connected to.
|
||||||
|
///
|
||||||
|
/// A focus can be any server powering the MatrixRTC session (SFU,
|
||||||
|
/// MCU). It serves as a node to redistribute RTC streams.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum Focus {
|
||||||
|
/// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
|
||||||
|
Livekit(LivekitFocus),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The struct to describe LiveKit as a `preferred_foci`.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LivekitFocus {
|
||||||
|
/// The alias where the LiveKit sessions can be reached.
|
||||||
|
#[serde(rename = "livekit_alias")]
|
||||||
|
pub alias: String,
|
||||||
|
|
||||||
|
/// The URL of the JWT service for the LiveKit instance.
|
||||||
|
#[serde(rename = "livekit_service_url")]
|
||||||
|
pub service_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LivekitFocus {
|
||||||
|
/// Initialize a [`LivekitFocus`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `alias` - The alias with which the LiveKit sessions can be reached.
|
||||||
|
/// * `service_url` - The url of the JWT server for the LiveKit instance.
|
||||||
|
pub fn new(alias: String, service_url: String) -> Self {
|
||||||
|
Self { alias, service_url }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data to define the actively used Focus.
|
||||||
|
///
|
||||||
|
/// A focus can be any server powering the MatrixRTC session (SFU,
|
||||||
|
/// MCU). It serves as a node to redistribute RTC streams.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum ActiveFocus {
|
||||||
|
/// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
|
||||||
|
Livekit(ActiveLivekitFocus),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fields to describe the `active_foci`.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct ActiveLivekitFocus {
|
||||||
|
/// The selection method used to select the LiveKit focus for the rtc session.
|
||||||
|
pub focus_select: FocusSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveLivekitFocus {
|
||||||
|
/// Initialize a [`ActiveLivekitFocus`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `focus_select` - The selection method used to select the LiveKit focus for the rtc
|
||||||
|
/// session.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { focus_select: FocusSelection::OldestMembership }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to select the active focus for LiveKit
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||||
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
|
#[ruma_enum(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub enum FocusSelection {
|
||||||
|
/// Select the active focus by using the oldest membership and the oldest focus.
|
||||||
|
OldestMembership,
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Custom(PrivOwnedStr),
|
||||||
|
}
|
319
crates/ruma-events/src/call/member/member_data.rs
Normal file
319
crates/ruma-events/src/call/member/member_data.rs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
//! Types for MatrixRTC `m.call.member` state event content data ([MSC3401])
|
||||||
|
//!
|
||||||
|
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use as_variant::as_variant;
|
||||||
|
use ruma_common::MilliSecondsSinceUnixEpoch;
|
||||||
|
use ruma_macros::StringEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use super::focus::{ActiveFocus, ActiveLivekitFocus, Focus};
|
||||||
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
|
/// The data object that contains the information for one membership.
|
||||||
|
///
|
||||||
|
/// It can be a legacy or a normal MatrixRTC Session membership.
|
||||||
|
///
|
||||||
|
/// The legacy format contains time information to compute if it is expired or not.
|
||||||
|
/// SessionMembershipData does not have the concept of timestamp based expiration anymore.
|
||||||
|
/// The state event will reliably be set to empty when the user disconnects.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub enum MembershipData<'a> {
|
||||||
|
/// The legacy format (using an array of memberships for each device -> one event per user)
|
||||||
|
Legacy(&'a LegacyMembershipData),
|
||||||
|
/// One event per device. `SessionMembershipData` contains all the information required to
|
||||||
|
/// represent the current membership state of one device.
|
||||||
|
Session(&'a SessionMembershipData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MembershipData<'a> {
|
||||||
|
/// The application this RTC membership participates in (the session type, can be `m.call`...)
|
||||||
|
pub fn application(&self) -> &Application {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.application,
|
||||||
|
MembershipData::Session(data) => &data.application,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
pub fn device_id(&self) -> &String {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.device_id,
|
||||||
|
MembershipData::Session(data) => &data.device_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active focus is a FocusType specific object that describes how this user
|
||||||
|
/// is currently connected.
|
||||||
|
///
|
||||||
|
/// It can use the foci_preferred list to choose one of the available (preferred)
|
||||||
|
/// foci or specific information on how to connect to this user.
|
||||||
|
///
|
||||||
|
/// Every user needs to converge to use the same focus_active type.
|
||||||
|
pub fn focus_active(&self) -> &ActiveFocus {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(_) => &ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: super::focus::FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
MembershipData::Session(data) => &data.focus_active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of available/preferred options this user provides to connect to the call.
|
||||||
|
pub fn foci_preferred(&self) -> &Vec<Focus> {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.foci_active,
|
||||||
|
MembershipData::Session(data) => &data.foci_preferred,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application of the membership is "m.call" and the scope is "m.room".
|
||||||
|
pub fn is_room_call(&self) -> bool {
|
||||||
|
as_variant!(self.application(), Application::Call)
|
||||||
|
.is_some_and(|call| call.scope == CallScope::Room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application of the membership is "m.call".
|
||||||
|
pub fn is_call(&self) -> bool {
|
||||||
|
as_variant!(self.application(), Application::Call).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the event is expired. This is only relevant for LegacyMembershipData
|
||||||
|
/// returns `false` if its SessionMembershipData
|
||||||
|
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => data.is_expired(origin_server_ts),
|
||||||
|
MembershipData::Session(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the created_ts of the event.
|
||||||
|
///
|
||||||
|
/// This is the `origin_server_ts` for session data.
|
||||||
|
/// For legacy events this can either be the origin server ts or a copy from the
|
||||||
|
/// `origin_server_ts` since we expect legacy events to get updated (when a new device
|
||||||
|
/// joins/leaves).
|
||||||
|
pub fn created_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => data.created_ts,
|
||||||
|
MembershipData::Session(data) => data.created_ts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A membership describes one of the sessions this user currently partakes.
|
||||||
|
///
|
||||||
|
/// The application defines the type of the session.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LegacyMembershipData {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// The duration in milliseconds relative to the time this membership joined
|
||||||
|
/// during which the membership is valid.
|
||||||
|
///
|
||||||
|
/// The time a member has joined is defined as:
|
||||||
|
/// `MIN(content.created_ts, event.origin_server_ts)`
|
||||||
|
#[serde(with = "ruma_common::serde::duration::ms")]
|
||||||
|
pub expires: Duration,
|
||||||
|
|
||||||
|
/// Stores a copy of the `origin_server_ts` of the initial session event.
|
||||||
|
///
|
||||||
|
/// If the membership is updated this field will be used to track to
|
||||||
|
/// original `origin_server_ts`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
|
||||||
|
/// A list of the foci in use for this membership.
|
||||||
|
pub foci_active: Vec<Focus>,
|
||||||
|
|
||||||
|
/// The id of the membership.
|
||||||
|
///
|
||||||
|
/// This is required to guarantee uniqueness of the event.
|
||||||
|
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
||||||
|
/// 200.
|
||||||
|
#[serde(rename = "membershipID")]
|
||||||
|
pub membership_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyMembershipData {
|
||||||
|
/// Checks if the event is expired.
|
||||||
|
///
|
||||||
|
/// Defaults to using `created_ts` of the [`LegacyMembershipData`].
|
||||||
|
/// If no `origin_server_ts` is provided and the event does not contain `created_ts`
|
||||||
|
/// the event will be considered as not expired.
|
||||||
|
/// In this case, a warning will be logged.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `origin_server_ts` - a fallback if [`LegacyMembershipData::created_ts`] is not present
|
||||||
|
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
||||||
|
let ev_created_ts = self.created_ts.or(origin_server_ts);
|
||||||
|
|
||||||
|
if let Some(ev_created_ts) = ev_created_ts {
|
||||||
|
let now = MilliSecondsSinceUnixEpoch::now().to_system_time();
|
||||||
|
let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires);
|
||||||
|
now > expire_ts
|
||||||
|
} else {
|
||||||
|
// This should not be reached since we only allow events that have copied over
|
||||||
|
// the origin server ts. `set_created_ts_if_none`
|
||||||
|
warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\
|
||||||
|
It is treated as a non expired event but this might be wrong.");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initial set of fields of [`LegacyMembershipData`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
pub struct LegacyMembershipDataInit {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// The duration in milliseconds relative to the time this membership joined
|
||||||
|
/// during which the membership is valid.
|
||||||
|
///
|
||||||
|
/// The time a member has joined is defined as:
|
||||||
|
/// `MIN(content.created_ts, event.origin_server_ts)`
|
||||||
|
pub expires: Duration,
|
||||||
|
|
||||||
|
/// A list of the focuses (foci) in use for this membership.
|
||||||
|
pub foci_active: Vec<Focus>,
|
||||||
|
|
||||||
|
/// The id of the membership.
|
||||||
|
///
|
||||||
|
/// This is required to guarantee uniqueness of the event.
|
||||||
|
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
||||||
|
/// 200.
|
||||||
|
pub membership_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LegacyMembershipDataInit> for LegacyMembershipData {
|
||||||
|
fn from(init: LegacyMembershipDataInit) -> Self {
|
||||||
|
let LegacyMembershipDataInit {
|
||||||
|
application,
|
||||||
|
device_id,
|
||||||
|
expires,
|
||||||
|
foci_active,
|
||||||
|
membership_id,
|
||||||
|
} = init;
|
||||||
|
Self { application, device_id, expires, created_ts: None, foci_active, membership_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all the information for a MatrixRTC membership. (one for each device)
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct SessionMembershipData {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// A list of the foci that this membership proposes to use.
|
||||||
|
pub foci_preferred: Vec<Focus>,
|
||||||
|
|
||||||
|
/// Data required to determine the currently used focus by this member.
|
||||||
|
pub focus_active: ActiveFocus,
|
||||||
|
|
||||||
|
/// Stores a copy of the `origin_server_ts` of the initial session event.
|
||||||
|
///
|
||||||
|
/// This is not part of the serialized event and computed after serialization.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of the MatrixRTC session.
|
||||||
|
///
|
||||||
|
/// This is not the application/client used by the user but the
|
||||||
|
/// type of MatrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
|
||||||
|
/// possible applications.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "application")]
|
||||||
|
pub enum Application {
|
||||||
|
/// The rtc application (session type) for VoIP call.
|
||||||
|
#[serde(rename = "m.call")]
|
||||||
|
Call(CallApplicationContent),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call specific parameters of a `m.call.member` event.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct CallApplicationContent {
|
||||||
|
/// An identifier for calls.
|
||||||
|
///
|
||||||
|
/// All members using the same `call_id` will end up in the same call.
|
||||||
|
///
|
||||||
|
/// Does not need to be a uuid.
|
||||||
|
///
|
||||||
|
/// `""` is used for room scoped calls.
|
||||||
|
pub call_id: String,
|
||||||
|
|
||||||
|
/// Who owns/joins/controls (can modify) the call.
|
||||||
|
pub scope: CallScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CallApplicationContent {
|
||||||
|
/// Initialize a [`CallApplicationContent`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
|
||||||
|
/// the same call. Does not need to be a uuid. `""` is used for room scoped calls.
|
||||||
|
/// * `scope` - Who owns/joins/controls (can modify) the call.
|
||||||
|
pub fn new(call_id: String, scope: CallScope) -> Self {
|
||||||
|
Self { call_id, scope }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The call scope defines different call ownership models.
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||||
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_enum(rename_all = "m.snake_case")]
|
||||||
|
pub enum CallScope {
|
||||||
|
/// A call which every user of a room can join and create.
|
||||||
|
///
|
||||||
|
/// There is no particular name associated with it.
|
||||||
|
///
|
||||||
|
/// There can only be one per room.
|
||||||
|
Room,
|
||||||
|
|
||||||
|
/// A user call is owned by a user.
|
||||||
|
///
|
||||||
|
/// Each user can create one there can be multiple per room. They are started and ended by the
|
||||||
|
/// owning user.
|
||||||
|
User,
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Custom(PrivOwnedStr),
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
//! Type for the matrixRTC notify event ([MSC4075]).
|
//! Type for the MatrixRTC notify event ([MSC4075]).
|
||||||
//!
|
//!
|
||||||
//! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
|
//! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
|
||||||
|
|
||||||
@ -75,3 +75,99 @@ impl From<Application> for ApplicationType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
|
||||||
|
Mentions,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notify_event_serialization() {
|
||||||
|
use ruma_common::owned_user_id;
|
||||||
|
|
||||||
|
let content_user_mention = CallNotifyEventContent::new(
|
||||||
|
"abcdef".into(),
|
||||||
|
ApplicationType::Call,
|
||||||
|
NotifyType::Ring,
|
||||||
|
Mentions::with_user_ids(vec![
|
||||||
|
owned_user_id!("@user:example.com"),
|
||||||
|
owned_user_id!("@user2:example.com"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let content_room_mention = CallNotifyEventContent::new(
|
||||||
|
"abcdef".into(),
|
||||||
|
ApplicationType::Call,
|
||||||
|
NotifyType::Ring,
|
||||||
|
Mentions::with_room_mention(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&content_user_mention).unwrap(),
|
||||||
|
json!({
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": {
|
||||||
|
"user_ids": ["@user2:example.com","@user:example.com"],
|
||||||
|
},
|
||||||
|
"notify_type": "ring",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&content_room_mention).unwrap(),
|
||||||
|
json!({
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": { "room": true },
|
||||||
|
"notify_type": "ring",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notify_event_deserialization() {
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use assert_matches2::assert_matches;
|
||||||
|
use ruma_common::owned_user_id;
|
||||||
|
|
||||||
|
use crate::{AnyMessageLikeEvent, MessageLikeEvent};
|
||||||
|
|
||||||
|
let json_data = json!({
|
||||||
|
"content": {
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": {
|
||||||
|
"room": false,
|
||||||
|
"user_ids": ["@user:example.com", "@user2:example.com"],
|
||||||
|
},
|
||||||
|
"notify_type": "ring",
|
||||||
|
},
|
||||||
|
"event_id": "$event:notareal.hs",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "m.call.notify",
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
event,
|
||||||
|
AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event))
|
||||||
|
);
|
||||||
|
let content = message_event.content;
|
||||||
|
assert_eq!(content.call_id, "abcdef");
|
||||||
|
assert!(!content.mentions.room);
|
||||||
|
assert_eq!(
|
||||||
|
content.mentions.user_ids,
|
||||||
|
BTreeSet::from([
|
||||||
|
owned_user_id!("@user:example.com"),
|
||||||
|
owned_user_id!("@user2:example.com")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use assign::assign;
|
use assign::assign;
|
||||||
@ -8,11 +5,6 @@ use js_int::uint;
|
|||||||
use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use ruma_events::call::CallCapabilities;
|
use ruma_events::call::CallCapabilities;
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
use ruma_events::{
|
|
||||||
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
|
|
||||||
Mentions,
|
|
||||||
};
|
|
||||||
use ruma_events::{
|
use ruma_events::{
|
||||||
call::{
|
call::{
|
||||||
answer::CallAnswerEventContent,
|
answer::CallAnswerEventContent,
|
||||||
@ -616,83 +608,3 @@ fn select_v1_answer_event_deserialization() {
|
|||||||
assert_eq!(content.selected_party_id, "6336");
|
assert_eq!(content.selected_party_id, "6336");
|
||||||
assert_eq!(content.version, VoipVersionId::V1);
|
assert_eq!(content.version, VoipVersionId::V1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
#[test]
|
|
||||||
fn notify_event_serialization() {
|
|
||||||
use ruma_common::owned_user_id;
|
|
||||||
|
|
||||||
let content_user_mention = CallNotifyEventContent::new(
|
|
||||||
"abcdef".into(),
|
|
||||||
ApplicationType::Call,
|
|
||||||
NotifyType::Ring,
|
|
||||||
Mentions::with_user_ids(vec![
|
|
||||||
owned_user_id!("@user:example.com"),
|
|
||||||
owned_user_id!("@user2:example.com"),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let content_room_mention = CallNotifyEventContent::new(
|
|
||||||
"abcdef".into(),
|
|
||||||
ApplicationType::Call,
|
|
||||||
NotifyType::Ring,
|
|
||||||
Mentions::with_room_mention(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
to_json_value(&content_user_mention).unwrap(),
|
|
||||||
json!({
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": {
|
|
||||||
"user_ids": ["@user2:example.com","@user:example.com"],
|
|
||||||
},
|
|
||||||
"notify_type": "ring",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
to_json_value(&content_room_mention).unwrap(),
|
|
||||||
json!({
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": { "room": true },
|
|
||||||
"notify_type": "ring",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
#[test]
|
|
||||||
fn notify_event_deserialization() {
|
|
||||||
use ruma_common::owned_user_id;
|
|
||||||
|
|
||||||
let json_data = json!({
|
|
||||||
"content": {
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": {
|
|
||||||
"room": false,
|
|
||||||
"user_ids": ["@user:example.com", "@user2:example.com"],
|
|
||||||
},
|
|
||||||
"notify_type": "ring",
|
|
||||||
},
|
|
||||||
"event_id": "$event:notareal.hs",
|
|
||||||
"origin_server_ts": 134_829_848,
|
|
||||||
"room_id": "!roomid:notareal.hs",
|
|
||||||
"sender": "@user:notareal.hs",
|
|
||||||
"type": "m.call.notify",
|
|
||||||
});
|
|
||||||
|
|
||||||
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
|
||||||
assert_matches!(
|
|
||||||
event,
|
|
||||||
AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event))
|
|
||||||
);
|
|
||||||
let content = message_event.content;
|
|
||||||
assert_eq!(content.call_id, "abcdef");
|
|
||||||
assert!(!content.mentions.room);
|
|
||||||
assert_eq!(
|
|
||||||
content.mentions.user_ids,
|
|
||||||
BTreeSet::from([owned_user_id!("@user:example.com"), owned_user_id!("@user2:example.com")])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user