632 lines
28 KiB
JavaScript
632 lines
28 KiB
JavaScript
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
|
|
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 { logger as rootLogger } from "../logger.js";
|
|
import { TypedEventEmitter } from "../models/typed-event-emitter.js";
|
|
import { EventTimeline } from "../models/event-timeline.js";
|
|
import { EventType, RelationType } from "../@types/event.js";
|
|
import { KnownMembership } from "../@types/membership.js";
|
|
import { CallMembership } from "./CallMembership.js";
|
|
import { RoomStateEvent } from "../models/room-state.js";
|
|
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.js";
|
|
import { logDurationSync } from "../utils.js";
|
|
import { MembershipManagerEvent } from "./IMembershipManager.js";
|
|
import { RTCEncryptionManager } from "./RTCEncryptionManager.js";
|
|
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.js";
|
|
import { TypedReEmitter } from "../ReEmitter.js";
|
|
import { RoomStickyEventsEvent } from "../models/room-sticky-events.js";
|
|
import { computeSlotId } from "./utils.js";
|
|
|
|
/**
|
|
* Events emitted by MatrixRTCSession
|
|
*/
|
|
export let MatrixRTCSessionEvent = /*#__PURE__*/function (MatrixRTCSessionEvent) {
|
|
// A member joined, left, or updated a property of their membership.
|
|
MatrixRTCSessionEvent["MembershipsChanged"] = "memberships_changed";
|
|
// We joined or left the session: our own local idea of whether we are joined,
|
|
// separate from MembershipsChanged, ie. independent of whether our member event
|
|
// has successfully gone through.
|
|
MatrixRTCSessionEvent["JoinStateChanged"] = "join_state_changed";
|
|
// The key used to encrypt media has changed
|
|
MatrixRTCSessionEvent["EncryptionKeyChanged"] = "encryption_key_changed";
|
|
/** The membership manager had to shut down caused by an unrecoverable error */
|
|
MatrixRTCSessionEvent["MembershipManagerError"] = "membership_manager_error";
|
|
/** The RTCSession did send a call notification caused by joining the call as the first member */
|
|
MatrixRTCSessionEvent["DidSendCallNotification"] = "did_send_call_notification";
|
|
return MatrixRTCSessionEvent;
|
|
}({});
|
|
|
|
// The names follow these principles:
|
|
// - we use the technical term delay if the option is related to delayed events.
|
|
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
|
// - we use membershipEvent if the option is related to the rtc member state event.
|
|
// - we use the technical term expiry if the option is related to the expiry field of the membership state event.
|
|
// - we use a `Ms` postfix if the option is a duration to avoid using words like:
|
|
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
|
|
|
|
const DEFAULT_SESSION_MEMBERSHIPS_FOR_SLOT_OPTS = {
|
|
listenForStickyEvents: true,
|
|
listenForMemberStateEvents: true
|
|
};
|
|
|
|
/**
|
|
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
|
* This class doesn't deal with media at all, just membership & properties of a session.
|
|
*/
|
|
export class MatrixRTCSession extends TypedEventEmitter {
|
|
get membershipStatus() {
|
|
return this.membershipManager?.status;
|
|
}
|
|
get probablyLeft() {
|
|
return this.membershipManager?.probablyLeft;
|
|
}
|
|
get delayId() {
|
|
return this.membershipManager?.delayId;
|
|
}
|
|
|
|
/**
|
|
* The callId (sessionId) of the call.
|
|
*
|
|
* It can be undefined since the callId is only known once the first membership joins.
|
|
* The callId is the property that, per definition, groups memberships into one call.
|
|
* @deprecated use `slotId` instead.
|
|
*/
|
|
get callId() {
|
|
return this.slotDescription?.id;
|
|
}
|
|
/**
|
|
* The slotId of the call.
|
|
* `{application}#{appSpecificId}`
|
|
* It can be undefined since the slotId is only known once the first membership joins.
|
|
* The slotId is the property that, per definition, groups memberships into one call.
|
|
*/
|
|
get slotId() {
|
|
return computeSlotId(this.slotDescription);
|
|
}
|
|
|
|
/**
|
|
* Reads the current slot state event's content for this session.
|
|
*
|
|
* @returns The slot event's content, or `undefined` if no slot event exists for this session.
|
|
*/
|
|
getRtcSlot() {
|
|
return getSlotEventContent(this.roomSubset, this.slotDescription);
|
|
}
|
|
|
|
/**
|
|
* Whether this session's slot is closed.
|
|
*
|
|
* @returns `true` if the slot is closed, `false` if the slot is open or `undefined`
|
|
* if no slot exists.
|
|
*/
|
|
isSlotClosed() {
|
|
return isSlotClosed(this.roomSubset, this.slotDescription);
|
|
}
|
|
|
|
/**
|
|
* Returns all the call memberships for a room that match the provided `sessionDescription`,
|
|
* oldest first.
|
|
*
|
|
* By default, this will return *both* sticky and member state events.
|
|
*/
|
|
static async sessionMembershipsForSlot(room, slotDescription,
|
|
// default both true this implied we combine sticky and state events for the final call state
|
|
// (prefer sticky events in case of a duplicate)
|
|
options = DEFAULT_SESSION_MEMBERSHIPS_FOR_SLOT_OPTS) {
|
|
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId} ${slotDescription.application}#${slotDescription.id}]`);
|
|
const callMemberEvents = collectMembersEvents(room, slotDescription, options, logger);
|
|
const callMemberships = await computeBackendIdentityAndVerifyMemberEvents(room, callMemberEvents, slotDescription, logger);
|
|
callMemberships.sort((a, b) => a.createdTs() - b.createdTs());
|
|
if (callMemberships.length > 1) {
|
|
logger.debug(`Call memberships in room ${room.roomId}, in order: `, callMemberships.map(m => [m.createdTs(), m.userId]));
|
|
}
|
|
return callMemberships;
|
|
}
|
|
|
|
/**
|
|
* Return the MatrixRTC session for the room.
|
|
* This returned session can be used to find out if there are active sessions
|
|
* for the requested room and `slotDescription`.
|
|
*/
|
|
static sessionForSlot(client, room, slotDescription, opts) {
|
|
return new MatrixRTCSession(client, room, slotDescription, opts);
|
|
}
|
|
|
|
/**
|
|
* WARN: this can in theory only be a subset of the room with the properties required by
|
|
* this class.
|
|
* Outside of tests this most likely will be a full room, however.
|
|
* @deprecated Relying on a full Room object being available here is an anti-pattern. You should be tracking
|
|
* the room object in your own code and passing it in when needed.
|
|
*/
|
|
get room() {
|
|
return this.roomSubset;
|
|
}
|
|
|
|
/**
|
|
* This constructs a room session. When using MatrixRTC inside the js-sdk this is expected
|
|
* to be used with the MatrixRTCSessionManager exclusively.
|
|
*
|
|
* In cases where you don't use the js-sdk but build on top of another Matrix stack this class can be used standalone
|
|
* to manage a joined MatrixRTC session.
|
|
*
|
|
* @param client A subset of the {@link MatrixClient} that lets the session interact with the Matrix room.
|
|
* @param roomSubset The room this session is attached to. A subset of a js-sdk Room that the session needs.
|
|
* @param slotDescription The slot description is a virtual address where participants are allowed to meet.
|
|
* This session will only manage memberships that match this slot description.Sessions are distinct if any of
|
|
* those properties are distinct: `roomSubset.roomId`, `slotDescription.application`, `slotDescription.id`.
|
|
* @param calculateMembershipsOpts - Options to configure how memberships are calculated for this session.
|
|
*/
|
|
constructor(client, roomSubset, slotDescription, calculateMembershipsOpts) {
|
|
super();
|
|
_defineProperty(this, "membershipManager", void 0);
|
|
_defineProperty(this, "encryptionManager", void 0);
|
|
_defineProperty(this, "joinConfig", void 0);
|
|
_defineProperty(this, "logger", void 0);
|
|
_defineProperty(this, "pendingNotificationToSend", void 0);
|
|
/**
|
|
* This timeout is responsible to track any expiration. We need to know when we have to start
|
|
* to ignore other call members. There is no callback for this. This timeout will always be configured to
|
|
* emit when the next membership expires.
|
|
*/
|
|
_defineProperty(this, "expiryTimeout", void 0);
|
|
_defineProperty(this, "memberships", []);
|
|
/**
|
|
* Resolves when the session has calculated the initial membership of the session.
|
|
*/
|
|
_defineProperty(this, "initialMembershipCalculated", void 0);
|
|
/**
|
|
* Does membership need to be recalculated? This is set to false upon
|
|
* recalculation.
|
|
*/
|
|
_defineProperty(this, "membershipNeedsRecalculation", false);
|
|
/**
|
|
* The statistics for this session.
|
|
*/
|
|
_defineProperty(this, "statistics", {
|
|
counters: {
|
|
roomEventEncryptionKeysSent: 0,
|
|
roomEventEncryptionKeysReceived: 0
|
|
},
|
|
totals: {
|
|
roomEventEncryptionKeysReceivedTotalAge: 0
|
|
}
|
|
});
|
|
_defineProperty(this, "reEmitter", new TypedReEmitter(this));
|
|
/**
|
|
* Call this when the Matrix room members have changed.
|
|
*/
|
|
_defineProperty(this, "onRoomMemberUpdate", () => {
|
|
void this.ensureRecalculateSessionMembers();
|
|
});
|
|
/**
|
|
* Call this when a room state event has been updated.
|
|
*/
|
|
_defineProperty(this, "onRoomStateEvent", event => {
|
|
if (event.getType() !== EventType.RTCSlot) return;
|
|
if (event.getStateKey() !== computeSlotId(this.slotDescription)) return;
|
|
void this.ensureRecalculateSessionMembers();
|
|
});
|
|
/**
|
|
* Call this when a sticky event update has occured.
|
|
*/
|
|
_defineProperty(this, "onStickyEventUpdate", (added, updated, removed) => {
|
|
if ([...added, ...removed, ...updated.flatMap(v => [v.current, v.previous])].some(e => e.getType() === EventType.RTCMembership)) {
|
|
void this.ensureRecalculateSessionMembers();
|
|
}
|
|
});
|
|
/**
|
|
* Call this when something changed that may impacts the current MatrixRTC members in this session.
|
|
*/
|
|
// We allow this name schema since this function should only be used for testing purposes.
|
|
_defineProperty(this, "_onRTCSessionMemberUpdate", async () => {
|
|
await this.recalculateSessionMembers();
|
|
});
|
|
// helper variables to make sure we do not have parallel running recalculations.
|
|
_defineProperty(this, "recalculateSessionMembersPromise", Promise.resolve());
|
|
/**
|
|
* Call this when anything that could impact rtc memberships has changed: Room Members or RTC members.
|
|
*
|
|
* Examines the latest call memberships and handles any encryption key sending or rotation that is needed.
|
|
*
|
|
* This function should be called when the room members or call memberships might have changed.
|
|
*/
|
|
_defineProperty(this, "recalculateSessionMembers", async () => {
|
|
// Clear the flag.
|
|
this.membershipNeedsRecalculation = false;
|
|
const oldMemberships = this.memberships;
|
|
this.memberships = await MatrixRTCSession.sessionMembershipsForSlot(this.room, this.slotDescription, this.calculateMembershipsOpts);
|
|
const changed = oldMemberships.length != this.memberships.length || oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i]));
|
|
if (changed) {
|
|
this.logger.info(`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`);
|
|
logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
|
|
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
|
|
});
|
|
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
|
|
// The `ownMembership` will be set when calling `onRTCSessionMemberUpdate`.
|
|
const ownMembership = this.membershipManager?.ownMembership;
|
|
if (this.pendingNotificationToSend && ownMembership && oldMemberships.length === 0) {
|
|
// If we're the first member in the call, we're responsible for
|
|
// sending the notification event
|
|
if (ownMembership.eventId && this.joinConfig?.notificationType) {
|
|
this.sendCallNotify(ownMembership.eventId, this.joinConfig.notificationType, ownMembership.callIntent);
|
|
} else {
|
|
this.logger.warn("Own membership eventId is undefined, cannot send call notification");
|
|
}
|
|
}
|
|
// If anyone else joins the session it is no longer our responsibility to send the notification.
|
|
// (If we were the joiner we already did sent the notification in the block above.)
|
|
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
|
|
} else {
|
|
this.logger.debug(`No membership changes detected for room ${this.roomSubset.roomId}`);
|
|
}
|
|
// This also needs to be done if `changed` = false
|
|
// A member might have updated their fingerprint (created_ts)
|
|
this.encryptionManager?.onMembershipsUpdate(oldMemberships);
|
|
this.setExpiryTimer();
|
|
});
|
|
this.client = client;
|
|
this.roomSubset = roomSubset;
|
|
this.slotDescription = slotDescription;
|
|
this.calculateMembershipsOpts = calculateMembershipsOpts;
|
|
this.logger = rootLogger.getChild(`[MatrixRTCSession ${roomSubset.roomId} ${slotDescription.application}#${slotDescription.id}]`);
|
|
this.roomSubset.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
|
this.roomSubset.on(RoomStateEvent.Events, this.onRoomStateEvent);
|
|
this.roomSubset.on(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
|
|
this.initialMembershipCalculated = this.ensureRecalculateSessionMembers();
|
|
this.setExpiryTimer();
|
|
}
|
|
/*
|
|
* Returns true if we intend to be participating in the MatrixRTC session.
|
|
* This is determined by checking if the relativeExpiry has been set.
|
|
*/
|
|
isJoined() {
|
|
return this.membershipManager?.isJoined() ?? false;
|
|
}
|
|
|
|
/**
|
|
* Performs cleanup & removes timers for client shutdown
|
|
*/
|
|
async stop() {
|
|
await this.membershipManager?.leave(1000);
|
|
if (this.expiryTimeout) {
|
|
clearTimeout(this.expiryTimeout);
|
|
this.expiryTimeout = undefined;
|
|
}
|
|
this.roomSubset.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
|
this.roomSubset.off(RoomStateEvent.Events, this.onRoomStateEvent);
|
|
this.roomSubset.off(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
|
|
}
|
|
/**
|
|
* Announces this user and device as joined to the MatrixRTC session,
|
|
* and continues to update the membership event to keep it valid until
|
|
* leaveRoomSession() is called
|
|
* This will not subscribe to updates: remember to call subscribe() separately if
|
|
* desired.
|
|
* This method will return immediately and the session will be joined in the background.
|
|
* @param ownMembershipIdentity the identity of the user and device joining the session.
|
|
* This will be put into the content.member.
|
|
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
|
* If multiSfuFocus is set, this is only needed if this client wants to publish to multiple transports simultaneously.
|
|
* @param multiSfuFocus the active focus to use in the joined RTC membership event. Setting this implies the
|
|
* membership manager will operate in a multi-SFU connection mode. If `undefined`, an `oldest_membership`
|
|
* transport selection will be used instead.
|
|
* @param joinConfig - Additional configuration for the joined session.
|
|
*/
|
|
joinRTCSession(ownMembershipIdentity, fociPreferred, multiSfuFocus, joinConfig) {
|
|
if (this.isJoined()) {
|
|
this.logger.info(`Already joined to session in room ${this.roomSubset.roomId}: ignoring join call`);
|
|
return;
|
|
} else {
|
|
// Create MembershipManager and pass the RTCSession logger (with room id info)
|
|
this.membershipManager = joinConfig?.unstableSendStickyEvents ? new StickyEventMembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, ownMembershipIdentity.memberId, this.logger) : new MembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger);
|
|
this.reEmitter.reEmit(this.membershipManager, [MembershipManagerEvent.ProbablyLeft, MembershipManagerEvent.StatusChanged, MembershipManagerEvent.DelayIdChanged]);
|
|
// Create Encryption manager
|
|
const [room, client, statistics] = [this.roomSubset, this.client, this.statistics];
|
|
const transport = new ToDeviceKeyTransport(ownMembershipIdentity, room.roomId, client, statistics);
|
|
this.encryptionManager = new RTCEncryptionManager(ownMembershipIdentity, () => this.memberships, transport, (keyBin, encryptionKeyIndex, membership, rtcBackendIdentity) => {
|
|
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, membership, rtcBackendIdentity);
|
|
}, this.logger);
|
|
}
|
|
this.joinConfig = joinConfig;
|
|
this.pendingNotificationToSend = this.joinConfig?.notificationType;
|
|
|
|
// Join!
|
|
this.membershipManager.join(fociPreferred, multiSfuFocus, e => {
|
|
this.logger.error("MembershipManager encountered an unrecoverable error: ", e);
|
|
this.emit(MatrixRTCSessionEvent.MembershipManagerError, e);
|
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, this.isJoined());
|
|
});
|
|
this.encryptionManager.join(joinConfig);
|
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, true);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param fociPreferred
|
|
* @param multiSfuFocus
|
|
* @param joinConfig
|
|
* @deprecated use the joinRTCSession method instead
|
|
*/
|
|
joinRoomSession(fociPreferred, multiSfuFocus, joinConfig) {
|
|
const [userId, deviceId] = [this.client.getUserId(), this.client.getDeviceId()];
|
|
// TODO this wants to become a UUID
|
|
const memberId = `${userId}:${deviceId}`;
|
|
this.joinRTCSession({
|
|
userId,
|
|
deviceId,
|
|
memberId
|
|
}, fociPreferred, multiSfuFocus, joinConfig);
|
|
}
|
|
|
|
/**
|
|
* Announces this user and device as having left the MatrixRTC session
|
|
* and stops scheduled updates.
|
|
* This will not unsubscribe from updates: remember to call unsubscribe() separately if
|
|
* desired.
|
|
* The membership update required to leave the session will retry if it fails.
|
|
* Without network connection the promise will never resolve.
|
|
* A timeout can be provided so that there is a guarantee for the promise to resolve.
|
|
* @returns Whether the membership update was attempted and did not time out.
|
|
*/
|
|
async leaveRoomSession(timeout = undefined) {
|
|
if (!this.isJoined()) {
|
|
this.logger.info(`Not joined to session in room ${this.roomSubset.roomId}: ignoring leave call`);
|
|
return false;
|
|
}
|
|
this.logger.info(`Leaving call session in room ${this.roomSubset.roomId}`);
|
|
this.encryptionManager.leave();
|
|
const leavePromise = this.membershipManager.leave(timeout);
|
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
|
|
return await leavePromise;
|
|
}
|
|
/**
|
|
* This returns the focus in use by the oldest membership.
|
|
* Do not use since this might be just the focus for the oldest membership. others might use a different focus.
|
|
* @deprecated use `member.getTransport(session.getOldestMembership())` instead for the specific member you want to get the focus for.
|
|
*/
|
|
getFocusInUse() {
|
|
const oldestMembership = this.getOldestMembership();
|
|
return oldestMembership?.getTransport(oldestMembership);
|
|
}
|
|
getOldestMembership() {
|
|
return this.memberships[0];
|
|
}
|
|
|
|
/**
|
|
* Get the call intent for the current call, based on what members are advertising. If one or more
|
|
* members disagree on the current call intent, or nobody specifies one then `undefined` is returned.
|
|
*
|
|
* If all members that specify a call intent agree, that value is returned.
|
|
* @returns A call intent, or `undefined` if no consensus or not given.
|
|
*/
|
|
getConsensusCallIntent() {
|
|
const getFirstCallIntent = this.memberships.find(m => !!m.callIntent)?.callIntent;
|
|
if (!getFirstCallIntent) {
|
|
return undefined;
|
|
}
|
|
if (this.memberships.every(m => !m.callIntent || m.callIntent === getFirstCallIntent)) {
|
|
return getFirstCallIntent;
|
|
}
|
|
return undefined;
|
|
}
|
|
async updateCallIntent(callIntent) {
|
|
const myMembership = this.membershipManager?.ownMembership;
|
|
if (!myMembership) {
|
|
throw Error("Not connected yet");
|
|
}
|
|
await this.membershipManager?.updateCallIntent(callIntent);
|
|
}
|
|
|
|
/**
|
|
* Re-emit an EncryptionKeyChanged event for each tracked encryption key. This can be used to export
|
|
* the keys.
|
|
*/
|
|
reemitEncryptionKeys() {
|
|
this.encryptionManager?.getEncryptionKeys().forEach((keyRing, key) => {
|
|
keyRing.forEach(keyInfo => {
|
|
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyInfo.key, keyInfo.keyIndex, keyInfo.membership, keyInfo.rtcBackendIdentity);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets a timer for the soonest membership expiry
|
|
*/
|
|
setExpiryTimer() {
|
|
if (this.expiryTimeout) {
|
|
clearTimeout(this.expiryTimeout);
|
|
this.expiryTimeout = undefined;
|
|
}
|
|
let soonestExpiry;
|
|
for (const membership of this.memberships) {
|
|
const thisExpiry = membership.getMsUntilExpiry();
|
|
// If getMsUntilExpiry is undefined we have a MSC4143 (MatrixRTC) compliant event - it never expires
|
|
// but will be reliably resent on disconnect.
|
|
if (thisExpiry !== undefined && (soonestExpiry === undefined || thisExpiry < soonestExpiry)) {
|
|
soonestExpiry = thisExpiry;
|
|
}
|
|
}
|
|
if (soonestExpiry != undefined) {
|
|
this.expiryTimeout = setTimeout(() => void this.ensureRecalculateSessionMembers(), soonestExpiry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends notification events to indiciate the call has started.
|
|
* Note: This does not return a promise, instead scheduling the notification events to be sent.
|
|
* @param parentEventId Event id linking to your RTC call membership event.
|
|
* @param notificationType The type of notification to send
|
|
* @param callIntent The type of call this is (e.g. "audio").
|
|
*/
|
|
sendCallNotify(parentEventId, notificationType, callIntent) {
|
|
const lifetime = this.joinConfig?.notificationLifetimeMs ?? 90000;
|
|
const sendNotificationEvent = async () => {
|
|
const content = {
|
|
"m.mentions": {
|
|
user_ids: [],
|
|
room: true
|
|
},
|
|
"notification_type": notificationType,
|
|
"m.relates_to": {
|
|
event_id: parentEventId,
|
|
rel_type: RelationType.Reference
|
|
},
|
|
"sender_ts": Date.now(),
|
|
"lifetime": lifetime
|
|
};
|
|
if (callIntent) {
|
|
content["m.call.intent"] = callIntent;
|
|
}
|
|
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, content);
|
|
return {
|
|
response,
|
|
content
|
|
};
|
|
};
|
|
void sendNotificationEvent().then(notification => {
|
|
// Join event_id and origin event content
|
|
const newResult = _objectSpread(_objectSpread({}, notification.response), notification.content);
|
|
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult);
|
|
}).catch(([errorLegacy, errorNew]) => this.logger.error("Failed to send call notification", errorLegacy, errorNew));
|
|
}
|
|
/**
|
|
* Ensures that membership is recalculated when the state of the session may have changed.
|
|
* Also ensures that only one recalculation is made at a time.
|
|
* @returns A promise resolving when the state has been recalculated.
|
|
*/
|
|
ensureRecalculateSessionMembers() {
|
|
if (this.membershipNeedsRecalculation) {
|
|
// We have already requested recalcuation, don't attempt a new one.
|
|
return this.recalculateSessionMembersPromise;
|
|
}
|
|
this.membershipNeedsRecalculation = true;
|
|
// Chain the recalculation.
|
|
this.recalculateSessionMembersPromise = this.recalculateSessionMembersPromise.then(() => this.recalculateSessionMembers(), () => this.recalculateSessionMembers());
|
|
return this.recalculateSessionMembersPromise;
|
|
}
|
|
}
|
|
|
|
/// Private helpers
|
|
async function computeBackendIdentityAndVerifyMemberEvents(room, callMemberEvents, slotDescription, logger) {
|
|
const callMemberships = [];
|
|
for (const memberEvent of callMemberEvents) {
|
|
const content = memberEvent.getContent();
|
|
|
|
// Quick filter to avoid unneeded processing of invalid events or left events.
|
|
if (!quickFilterNonRelevantContents(content, logger)) {
|
|
continue;
|
|
}
|
|
try {
|
|
const membership = await CallMembership.parseFromEvent(memberEvent);
|
|
if (isValidMembership(membership, room, slotDescription, logger)) {
|
|
callMemberships.push(membership);
|
|
}
|
|
} catch (e) {
|
|
logger.warn("Couldn't construct call membership: ", e);
|
|
}
|
|
}
|
|
return callMemberships;
|
|
}
|
|
function quickFilterNonRelevantContents(content, logger) {
|
|
// Ignore sticky keys for the count
|
|
const eventKeysCount = Object.keys(content).filter(k => k !== "msc4354_sticky_key").length;
|
|
// Don't even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
|
|
if (eventKeysCount === 0) return false;
|
|
|
|
// We first decide if it's a MSC4143 event (per device state key)
|
|
if (eventKeysCount > 1 && "application" in content) {
|
|
// We have a MSC4143 event membership event with a proper joined content
|
|
return true;
|
|
} else if (eventKeysCount === 1 && "memberships" in content) {
|
|
// Events used to have this format in the past, but are now deprecated.
|
|
// Given that state events ~cannot be deleted, there can be some remaining events in the room, just ignore them.
|
|
return false;
|
|
} else {
|
|
// Invalid or left content
|
|
return false;
|
|
}
|
|
}
|
|
function isValidMembership(membership, room, slotDescription, logger) {
|
|
if (membership.slotDescription.id !== slotDescription.id) {
|
|
logger.info(`Ignoring membership of user ${membership.userId} for a different slot. Theirs: ${JSON.stringify(membership.slotDescription)}, Expected: ${JSON.stringify(slotDescription)}`);
|
|
return false;
|
|
}
|
|
if (membership.isExpired()) {
|
|
logger.info(`Ignoring expired device membership ${membership.userId}/${membership.deviceId}`);
|
|
return false;
|
|
}
|
|
if (!room.hasMembershipState(membership.userId ?? "", KnownMembership.Join)) {
|
|
logger.info(`Ignoring membership of user ${membership.userId} who is not in the room.`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reads the slot state event's content for the given slot description.
|
|
*
|
|
* @returns The slot event's content, or `undefined` if no slot event exists for the given description.
|
|
*/
|
|
function getSlotEventContent(room, slotDescription) {
|
|
const slotId = computeSlotId(slotDescription);
|
|
const slotEvent = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(EventType.RTCSlot, slotId);
|
|
if (!slotEvent) return undefined;
|
|
return slotEvent.getContent();
|
|
}
|
|
|
|
/**
|
|
* Whether the given slot is closed.
|
|
*
|
|
* @returns `true` if the slot is closed, `false` if the slot is open or `undefined`
|
|
* if no slot exists.
|
|
*/
|
|
function isSlotClosed(room, slotDescription) {
|
|
const content = getSlotEventContent(room, slotDescription);
|
|
if (content === undefined) return undefined;
|
|
return content.status !== "open" || typeof content.application !== "object" || content.application?.type !== slotDescription.application;
|
|
}
|
|
|
|
/**
|
|
* Collects the raw member events from room state and sticky store.
|
|
*/
|
|
function collectMembersEvents(room, slotDescription, options, logger) {
|
|
const {
|
|
listenForStickyEvents,
|
|
listenForMemberStateEvents
|
|
} = options;
|
|
let callMemberEvents = [];
|
|
if (listenForStickyEvents && !isSlotClosed(room, slotDescription)) {
|
|
// prefill with sticky events
|
|
callMemberEvents = [...room._unstable_getStickyEvents()].filter(e => e.getType() === EventType.RTCMembership);
|
|
}
|
|
if (listenForMemberStateEvents) {
|
|
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
|
if (!roomState) {
|
|
logger.warn("Couldn't get state for room " + room.roomId + "using empty membership array");
|
|
return [];
|
|
}
|
|
const callMemberStateEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix);
|
|
callMemberEvents = callMemberEvents.concat(callMemberStateEvents.filter(callMemberStateEvent => !callMemberEvents.some(
|
|
// only care about state events which have keys which we have not yet seen in the sticky events.
|
|
// TODO: I believe this can discard a joined state event if there is a matching left sticky event.
|
|
stickyEvent => stickyEvent.getContent().msc4354_sticky_key === callMemberStateEvent.getStateKey())));
|
|
}
|
|
return callMemberEvents;
|
|
}
|
|
//# sourceMappingURL=MatrixRTCSession.js.map
|