462 lines
16 KiB
JavaScript
462 lines
16 KiB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
/*
|
|
Copyright 2023-2026 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import { deepCompare } from "../utils.js";
|
|
import { logger } from "../logger.js";
|
|
import { computeSlotId, slotIdToDescription } from "./utils.js";
|
|
import { checkRtcMembershipData, computeRtcIdentityRaw, checkSessionsMembershipData, MatrixRTCMembershipParseError } from "./membershipData/index.js";
|
|
import { EventType } from "../@types/event.js";
|
|
|
|
/**
|
|
* The default duration in milliseconds that a membership is considered valid for.
|
|
* Ordinarily the client responsible for the session will update the membership before it expires.
|
|
* We use this duration as the fallback case where stale sessions are present for some reason.
|
|
*/
|
|
export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
|
|
|
|
/**
|
|
* Describes the source event type that provided the membership data.
|
|
*/
|
|
var MembershipKind = /*#__PURE__*/function (MembershipKind) {
|
|
/**
|
|
* The modern MSC4143 format event.
|
|
*/
|
|
MembershipKind["RTC"] = "rtc";
|
|
/**
|
|
* The legacy call event type.
|
|
*/
|
|
MembershipKind["Session"] = "session";
|
|
return MembershipKind;
|
|
}(MembershipKind || {});
|
|
// TODO: Rename to RtcMembership once we removed the legacy SessionMembership is removed, to avoid confusion.
|
|
export class CallMembership {
|
|
/**
|
|
* Parse the membershipdata from a call membership event.
|
|
* @param matrixEvent The Matrix event to read.
|
|
* @returns MembershipData in either MembershipKind.RTC or MembershipKind.Session format.
|
|
* @throws If the content is neither format.
|
|
*/
|
|
static membershipDataFromMatrixEvent(matrixEvent) {
|
|
const sender = matrixEvent.getSender();
|
|
const evType = matrixEvent.getType();
|
|
const data = matrixEvent.getContent();
|
|
if (sender === undefined) throw new Error("matrixEvent is missing sender field");
|
|
try {
|
|
// Event types are strictly checked here.
|
|
if (evType === EventType.RTCMembership && checkRtcMembershipData(data, sender)) {
|
|
return {
|
|
kind: MembershipKind.RTC,
|
|
data
|
|
};
|
|
} else if (evType === EventType.GroupCallMemberPrefix && checkSessionsMembershipData(data)) {
|
|
return {
|
|
kind: MembershipKind.Session,
|
|
data
|
|
};
|
|
} else {
|
|
throw Error(`'${evType} is not a known call membership type`);
|
|
}
|
|
} catch (ex) {
|
|
if (ex instanceof MatrixRTCMembershipParseError) {
|
|
logger.debug("CallMembership.MatrixRTCMembershipParseError provided invalid data", data);
|
|
}
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the contents of a MatrixEvent and create a CallMembership instance.
|
|
* @param matrixEvent The Matrix event to read.
|
|
*/
|
|
static async parseFromEvent(matrixEvent) {
|
|
const membershipData = this.membershipDataFromMatrixEvent(matrixEvent);
|
|
const rtcBackendIdentity = membershipData.kind === MembershipKind.RTC ? await computeRtcIdentityRaw(membershipData.data.member.user_id, membershipData.data.member.device_id, membershipData.data.member.id) : `${matrixEvent.getSender()}:${membershipData.data.device_id}`;
|
|
return new CallMembership(matrixEvent, membershipData, rtcBackendIdentity);
|
|
}
|
|
static equal(a, b) {
|
|
return deepCompare(a?.membershipData, b?.membershipData);
|
|
}
|
|
/**
|
|
* Use `parseFromEvent`.
|
|
* Constructor should only be used by tests.
|
|
* @private
|
|
* @param matrixEvent
|
|
* @param membershipData
|
|
* @param rtcBackendIdentity
|
|
*/
|
|
constructor(/** The Matrix event that this membership is based on */
|
|
matrixEvent, membershipData, rtcBackendIdentity) {
|
|
_defineProperty(this, "logger", void 0);
|
|
/** The parsed data from the Matrix event.
|
|
* To access checked eventId and sender from the matrixEvent.
|
|
* Class construction will fail if these values cannot get obtained. */
|
|
_defineProperty(this, "matrixEventData", void 0);
|
|
this.matrixEvent = matrixEvent;
|
|
this.membershipData = membershipData;
|
|
this.rtcBackendIdentity = rtcBackendIdentity;
|
|
const eventId = matrixEvent.getId();
|
|
const sender = matrixEvent.getSender();
|
|
if (eventId === undefined) throw new Error("parentEvent is missing eventId field");
|
|
if (sender === undefined) throw new Error("parentEvent is missing sender field");
|
|
this.logger = logger.getChild(`[CallMembership ${sender}:${this.deviceId}]`);
|
|
this.matrixEventData = {
|
|
eventId,
|
|
sender
|
|
};
|
|
}
|
|
|
|
/** @deprecated use userId instead */
|
|
get sender() {
|
|
return this.userId;
|
|
}
|
|
get userId() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.member.user_id;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return this.matrixEventData.sender;
|
|
}
|
|
}
|
|
get eventId() {
|
|
return this.matrixEventData.eventId;
|
|
}
|
|
|
|
/**
|
|
* The ID of the MatrixRTC slot that this membership belongs to (format `{application}#{id}`).
|
|
* This is computed in case SessionMembershipData is used.
|
|
*/
|
|
get slotId() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
if (data.application === "m.call") {
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.slot_id;
|
|
case MembershipKind.Session:
|
|
default:
|
|
{
|
|
const [application, id] = [data.application, data.call_id];
|
|
|
|
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
|
// The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
|
|
// State events still are sent with `""` however. To find other events that should end up in the same call,
|
|
// we use the slotId.
|
|
// Since the CallMembership is the public representation of a rtc.member event, we just pretend it is a
|
|
// "ROOM" slotId/call_id.
|
|
// This makes all the remote members work with just this simple trick.
|
|
//
|
|
// We of course now need to be careful when sending legacy events (state events)
|
|
// They get a slotDescription containing "ROOM" since this is what we use starting at the time this comment
|
|
// is commited.
|
|
//
|
|
// See the Other INFO_SLOT_ID_LEGACY_CASE comments to see where we revert back to "" just before sending the event.
|
|
let compatibilityAdaptedId;
|
|
if (id === "") {
|
|
compatibilityAdaptedId = "ROOM";
|
|
this.logger?.info("use slotId compat hack emptyString -> ROOM");
|
|
} else {
|
|
compatibilityAdaptedId = id;
|
|
}
|
|
return computeSlotId({
|
|
application,
|
|
id: compatibilityAdaptedId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
this.logger?.info("NOT using slotId compat hack emptyString -> ROOM");
|
|
// This is what the function should look like for any other application that did not
|
|
// go through a `""`=> `"ROOM"` rename
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.slot_id;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return computeSlotId({
|
|
application: data.application,
|
|
id: data.call_id
|
|
});
|
|
}
|
|
}
|
|
get deviceId() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.member.device_id;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return data.device_id;
|
|
}
|
|
}
|
|
get callIntent() {
|
|
const intent = this.applicationData["m.call.intent"];
|
|
if (typeof intent === "string") {
|
|
return intent;
|
|
}
|
|
this.logger.warn("RTC membership has invalid m.call.intent");
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Parsed `slot_id` (format `{application}#{id}`) into its components (application and id).
|
|
*/
|
|
get slotDescription() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
if (kind === MembershipKind.RTC) {
|
|
const id = data.slot_id.slice(`${data.application.type}#`.length);
|
|
return {
|
|
application: data.application.type,
|
|
id
|
|
};
|
|
}
|
|
return slotIdToDescription(this.slotId);
|
|
}
|
|
|
|
/**
|
|
* The application `type`.
|
|
* @deprecated Use @see applicationData
|
|
*/
|
|
get application() {
|
|
return this.applicationData.type;
|
|
}
|
|
|
|
/**
|
|
* Information about the application being used for the RTC session.
|
|
* May contain extra keys specific to the application.
|
|
*/
|
|
get applicationData() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.application;
|
|
case MembershipKind.Session:
|
|
default:
|
|
// SessionData does not have application data as such. We return specific
|
|
// properties in use by other getters in this class, for compatibility.
|
|
return {
|
|
"type": data.application,
|
|
"m.call.intent": data["m.call.intent"]
|
|
};
|
|
}
|
|
}
|
|
|
|
/** @deprecated scope is not used and will be removed in future versions. replaced by application specific types.*/
|
|
get scope() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return undefined;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return data.scope;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This computes the membership ID for the membership.
|
|
* For the sticky event based rtcSessionData this is trivial it is `member.id`.
|
|
* This is not supposed to be used to identity on an rtc backend. This is just a nouance for
|
|
* a generated (sha256) anonymised identity. Only send `rtcBackendIdentity` to any rtc backend service.
|
|
*
|
|
* For the legacy sessionMemberEvents it is a bit more complex. Here we sometimes do not have this data
|
|
* in the event content and we expected the SFU and the client to use `${this.matrixEventData.sender}:${data.device_id}`.
|
|
*
|
|
* So if there is no membershipID we use the hard coded jwt id default (`${this.matrixEventData.sender}:${data.device_id}`)
|
|
* value (used until version 0.16.0)
|
|
*
|
|
* It is also possible for a session event to set a custom membershipID. in that case this will be used.
|
|
*/
|
|
get memberId() {
|
|
// the createdTs behaves equivalent to the membershipID.
|
|
// we only need the field for the legacy member events where we needed to update them
|
|
// synapse ignores sending state events if they have the same content.
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case "rtc":
|
|
return data.member.id;
|
|
case "session":
|
|
return (
|
|
// best case we have a client already publishing the right custom membershipId
|
|
data.membershipID ??
|
|
// alternativly we use the hard coded jwt id defuatl value (used until version 0.16.0)
|
|
`${this.matrixEventData.sender}:${data.device_id}`
|
|
);
|
|
default:
|
|
throw Error("Not possible to get memberID without knowing the membership event kind");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated renamed to `memberId`
|
|
*/
|
|
get membershipID() {
|
|
return this.memberId;
|
|
}
|
|
createdTs() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
// TODO we need to read the referenced (relation) event if available to get the real created_ts
|
|
return this.matrixEvent.getTs();
|
|
case MembershipKind.Session:
|
|
default:
|
|
return data.created_ts ?? this.matrixEvent.getTs();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the absolute expiry timestamp of the membership.
|
|
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
|
*/
|
|
getAbsoluteExpiry() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return undefined;
|
|
case MembershipKind.Session:
|
|
default:
|
|
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
|
return this.createdTs() + (data.expires ?? DEFAULT_EXPIRE_DURATION);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
|
* @deprecated Not used by RTC events.
|
|
*/
|
|
getMsUntilExpiry() {
|
|
const {
|
|
kind
|
|
} = this.membershipData;
|
|
if (kind === MembershipKind.Session) {
|
|
const absExpiry = this.getAbsoluteExpiry();
|
|
if (absExpiry) {
|
|
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
|
|
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
|
|
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
|
|
return absExpiry - Date.now();
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @returns true if the membership has expired, otherwise false
|
|
*/
|
|
isExpired() {
|
|
const {
|
|
kind
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return false;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return this.getMsUntilExpiry() <= 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ## RTC Membership
|
|
* Gets the primary transport to use for this RTC membership (m.rtc.member).
|
|
* This will return the primary transport that is used by this call membership to publish their media.
|
|
* Directly relates to the `transports.published` field.
|
|
*
|
|
* ## Legacy session membership
|
|
* In case of a legacy session membership (m.call.member) this will return the selected transport where
|
|
* media is published. How this selection happens depends on the `focus_active` field of the session membership.
|
|
* If the `focus_selection` is `oldest_membership` this will return the transport of the oldest membership
|
|
* in the room (based on the `created_ts` field of the session membership).
|
|
* If the `focus_selection` is `multi_sfu` it will return the first transport of the `foci_preferred` list.
|
|
* (`multi_sfu` is equivalent to how `m.rtc.member` `transports.published` work).
|
|
* @param oldestMembership For backwards compatibility with session membership (legacy). Unused in case of RTC membership.
|
|
* Always required to make the consumer not care if it deals with RTC or session memberships.
|
|
* @returns The transport this membership uses to publish media or undefined if no transport is available.
|
|
*/
|
|
getTransport(oldestMembership) {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.transports.published[0];
|
|
case MembershipKind.Session:
|
|
switch (data.focus_active.focus_selection) {
|
|
case "oldest_membership":
|
|
if (CallMembership.equal(this, oldestMembership)) return data.foci_preferred[0];
|
|
if (oldestMembership !== undefined) return oldestMembership.getTransport(oldestMembership);
|
|
break;
|
|
case "multi_sfu":
|
|
return data.foci_preferred[0];
|
|
default:
|
|
// `focus_selection` not understood.
|
|
return undefined;
|
|
}
|
|
break;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The value of the `transports.published` field for RTC memberships (m.rtc.member).
|
|
* Or the value of the `foci_preferred` field for legacy session memberships (m.call.member).
|
|
*/
|
|
get transports() {
|
|
const {
|
|
kind,
|
|
data
|
|
} = this.membershipData;
|
|
switch (kind) {
|
|
case MembershipKind.RTC:
|
|
return data.transports.published;
|
|
case MembershipKind.Session:
|
|
default:
|
|
return data.foci_preferred;
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=CallMembership.js.map
|