init
This commit is contained in:
632
node_modules/matrix-js-sdk/lib/matrixrtc/MatrixRTCSession.js
generated
vendored
Normal file
632
node_modules/matrix-js-sdk/lib/matrixrtc/MatrixRTCSession.js
generated
vendored
Normal file
@@ -0,0 +1,632 @@
|
||||
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
|
||||
Reference in New Issue
Block a user