You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Merge branch 'develop' into matthew/rework-cross-signing-login
This commit is contained in:
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,3 +1,30 @@
|
|||||||
|
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
|
||||||
|
================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
|
||||||
|
|
||||||
|
* No changes since rc.1
|
||||||
|
|
||||||
|
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
|
||||||
|
|
||||||
|
* Optimise prefixed logger
|
||||||
|
[\#1615](https://github.com/matrix-org/matrix-js-sdk/pull/1615)
|
||||||
|
* Add debug logs to encryption prep, take 3
|
||||||
|
[\#1614](https://github.com/matrix-org/matrix-js-sdk/pull/1614)
|
||||||
|
* Add functions for upper & lowercase random strings
|
||||||
|
[\#1612](https://github.com/matrix-org/matrix-js-sdk/pull/1612)
|
||||||
|
* Room helpers for invite permissions and join rules
|
||||||
|
[\#1609](https://github.com/matrix-org/matrix-js-sdk/pull/1609)
|
||||||
|
* Fixed wording in "Adding video track with id" log
|
||||||
|
[\#1606](https://github.com/matrix-org/matrix-js-sdk/pull/1606)
|
||||||
|
* Add more debug logs to encryption prep
|
||||||
|
[\#1605](https://github.com/matrix-org/matrix-js-sdk/pull/1605)
|
||||||
|
* Add option to set ice candidate pool size
|
||||||
|
[\#1604](https://github.com/matrix-org/matrix-js-sdk/pull/1604)
|
||||||
|
* Cancel call if no source was selected
|
||||||
|
[\#1601](https://github.com/matrix-org/matrix-js-sdk/pull/1601)
|
||||||
|
|
||||||
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
|
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
|
||||||
================================================================================================
|
================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "9.7.0",
|
"version": "9.8.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||||
"lint": "yarn lint:types && yarn lint:js",
|
"lint": "yarn lint:types && yarn lint:js",
|
||||||
"lint:js": "eslint --max-warnings 73 src spec",
|
"lint:js": "eslint --max-warnings 72 src spec",
|
||||||
"lint:types": "tsc --noEmit",
|
"lint:types": "tsc --noEmit",
|
||||||
"test": "jest spec/ --coverage --testEnvironment node",
|
"test": "jest spec/ --coverage --testEnvironment node",
|
||||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||||
|
|||||||
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
|
|||||||
// new session and will have called claimOneTimeKeys
|
// new session and will have called claimOneTimeKeys
|
||||||
expect(count).toBe(2);
|
expect(count).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
|
||||||
|
// This test checks whether `ensureOlmSessionsForDevices` properly
|
||||||
|
// handles multiple tasks in flight ensuring some set of devices in
|
||||||
|
// common without deadlocks.
|
||||||
|
|
||||||
|
let claimRequestCount = 0;
|
||||||
|
const baseApis = {
|
||||||
|
claimOneTimeKeys: () => {
|
||||||
|
// simulate a very slow server (.5 seconds to respond)
|
||||||
|
claimRequestCount++;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(reject, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deviceBobA = DeviceInfo.fromStorage({
|
||||||
|
keys: {
|
||||||
|
"curve25519:BOB-A": "akey",
|
||||||
|
},
|
||||||
|
}, "BOB-A");
|
||||||
|
const deviceBobB = DeviceInfo.fromStorage({
|
||||||
|
keys: {
|
||||||
|
"curve25519:BOB-B": "bkey",
|
||||||
|
},
|
||||||
|
}, "BOB-B");
|
||||||
|
|
||||||
|
// There's no required ordering of devices per user, so here we
|
||||||
|
// create two different orderings so that each task reserves a
|
||||||
|
// device the other task needs before continuing.
|
||||||
|
const devicesByUserAB = {
|
||||||
|
"@bob:example.com": [
|
||||||
|
deviceBobA,
|
||||||
|
deviceBobB,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const devicesByUserBA = {
|
||||||
|
"@bob:example.com": [
|
||||||
|
deviceBobB,
|
||||||
|
deviceBobA,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function alwaysSucceed(promise) {
|
||||||
|
// swallow any exception thrown by a promise, so that
|
||||||
|
// Promise.all doesn't abort
|
||||||
|
return promise.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||||
|
aliceOlmDevice, baseApis, devicesByUserAB,
|
||||||
|
));
|
||||||
|
|
||||||
|
// After a single tick through the first task, it should have
|
||||||
|
// claimed ownership of all devices to avoid deadlocking others.
|
||||||
|
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||||
|
|
||||||
|
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||||
|
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||||
|
));
|
||||||
|
|
||||||
|
// The second task should not have changed the ownership count, as
|
||||||
|
// it's waiting on the first task.
|
||||||
|
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||||
|
|
||||||
|
// Track the tasks, but don't await them yet.
|
||||||
|
const promises = Promise.all([
|
||||||
|
task1,
|
||||||
|
task2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// After .2s, the first task should have made an initial claim request.
|
||||||
|
expect(claimRequestCount).toBe(1);
|
||||||
|
|
||||||
|
await promises;
|
||||||
|
|
||||||
|
// After waiting for both tasks to complete, the first task should
|
||||||
|
// have failed, so the second task should have tried to create a
|
||||||
|
// new session and will have called claimOneTimeKeys
|
||||||
|
expect(claimRequestCount).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,4 +190,62 @@ describe('Call', function() {
|
|||||||
// Hangup to stop timers
|
// Hangup to stop timers
|
||||||
call.hangup(CallErrorCode.UserHangup, true);
|
call.hangup(CallErrorCode.UserHangup, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add candidates received before answer if party ID is correct', async function() {
|
||||||
|
await call.placeVoiceCall();
|
||||||
|
call.peerConn.addIceCandidate = jest.fn();
|
||||||
|
|
||||||
|
call.onRemoteIceCandidatesReceived({
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
call_id: call.callId,
|
||||||
|
party_id: 'the_correct_party_id',
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
candidate: 'the_correct_candidate',
|
||||||
|
sdpMid: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
call.onRemoteIceCandidatesReceived({
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
call_id: call.callId,
|
||||||
|
party_id: 'some_other_party_id',
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
candidate: 'the_wrong_candidate',
|
||||||
|
sdpMid: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
await call.onAnswerReceived({
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
call_id: call.callId,
|
||||||
|
party_id: 'the_correct_party_id',
|
||||||
|
answer: {
|
||||||
|
sdp: DUMMY_SDP,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||||
|
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
|
||||||
|
candidate: 'the_correct_candidate',
|
||||||
|
sdpMid: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export enum EventType {
|
|||||||
*/
|
*/
|
||||||
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||||
|
|
||||||
|
// Spaces MSC1772
|
||||||
|
SpaceChild = "org.matrix.msc1772.space.child",
|
||||||
|
SpaceParent = "org.matrix.msc1772.space.parent",
|
||||||
|
|
||||||
// Room timeline events
|
// Room timeline events
|
||||||
RoomRedaction = "m.room.redaction",
|
RoomRedaction = "m.room.redaction",
|
||||||
RoomMessage = "m.room.message",
|
RoomMessage = "m.room.message",
|
||||||
@@ -87,3 +91,9 @@ export enum MsgType {
|
|||||||
Location = "m.location",
|
Location = "m.location",
|
||||||
Video = "m.video",
|
Video = "m.video",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
|
||||||
|
|
||||||
|
export enum RoomType {
|
||||||
|
Space = "org.matrix.msc1772.space", // Spaces MSC1772
|
||||||
|
}
|
||||||
|
|||||||
@@ -2389,3 +2389,26 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason)
|
|||||||
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
|
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches or paginates a summary of a space as defined by MSC2946
|
||||||
|
* @param {string} roomId The ID of the space-room to use as the root of the summary.
|
||||||
|
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
|
||||||
|
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
|
||||||
|
* @param {number?} limit The maximum number of rooms to return in total.
|
||||||
|
* @param {string?} batch The opaque token to paginate a previous summary request.
|
||||||
|
* @returns {Promise} the response, with next_batch, rooms, events fields.
|
||||||
|
*/
|
||||||
|
MatrixBaseApis.prototype.getSpaceSummary = function(roomId, maxRoomsPerSpace, autoJoinOnly, limit, batch) {
|
||||||
|
const path = utils.encodeUri("/rooms/$roomId/spaces", {
|
||||||
|
$roomId: roomId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._http.authedRequest(undefined, "POST", path, null, {
|
||||||
|
max_rooms_per_space: maxRoomsPerSpace,
|
||||||
|
auto_join_only: autoJoinOnly,
|
||||||
|
limit,
|
||||||
|
batch,
|
||||||
|
}, {
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -393,6 +393,9 @@ export function MatrixClient(opts) {
|
|||||||
this._clientWellKnown = undefined;
|
this._clientWellKnown = undefined;
|
||||||
this._clientWellKnownPromise = undefined;
|
this._clientWellKnownPromise = undefined;
|
||||||
|
|
||||||
|
this._turnServers = [];
|
||||||
|
this._turnServersExpiry = null;
|
||||||
|
|
||||||
// The SDK doesn't really provide a clean way for events to recalculate the push
|
// The SDK doesn't really provide a clean way for events to recalculate the push
|
||||||
// actions for themselves, so we have to kinda help them out when they are encrypted.
|
// actions for themselves, so we have to kinda help them out when they are encrypted.
|
||||||
// We do this so that push rules are correctly executed on events in their decrypted
|
// We do this so that push rules are correctly executed on events in their decrypted
|
||||||
@@ -4963,6 +4966,15 @@ MatrixClient.prototype.getTurnServers = function() {
|
|||||||
return this._turnServers || [];
|
return this._turnServers || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unix timestamp (in seconds) at which the current
|
||||||
|
* TURN credentials (from getTurnServers) expire
|
||||||
|
* @return {number} The expiry timestamp, in seconds, or null if no credentials
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.getTurnServersExpiry = function() {
|
||||||
|
return this._turnServersExpiry;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set whether to allow a fallback ICE server should be used for negotiating a
|
* Set whether to allow a fallback ICE server should be used for negotiating a
|
||||||
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
|
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
|
||||||
@@ -5434,6 +5446,9 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
|
|||||||
}));
|
}));
|
||||||
events = events.filter(e => e.getType() === eventType);
|
events = events.filter(e => e.getType() === eventType);
|
||||||
}
|
}
|
||||||
|
if (originalEvent && relationType === "m.replace") {
|
||||||
|
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
originalEvent,
|
originalEvent,
|
||||||
events,
|
events,
|
||||||
@@ -5458,6 +5473,7 @@ function checkTurnServers(client) {
|
|||||||
credential: res.password,
|
credential: res.password,
|
||||||
};
|
};
|
||||||
client._turnServers = [servers];
|
client._turnServers = [servers];
|
||||||
|
client._turnServersExpiry = Date.now() + res.ttl;
|
||||||
// re-fetch when we're about to reach the TTL
|
// re-fetch when we're about to reach the TTL
|
||||||
client._checkTurnServersTimeoutID = setTimeout(() => {
|
client._checkTurnServersTimeoutID = setTimeout(() => {
|
||||||
checkTurnServers(client);
|
checkTurnServers(client);
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[createOutboundSession]"),
|
||||||
);
|
);
|
||||||
return newSessionId;
|
return newSessionId;
|
||||||
};
|
};
|
||||||
@@ -605,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[createInboundSession]"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -619,8 +621,10 @@ OlmDevice.prototype.createInboundSession = async function(
|
|||||||
* @return {Promise<string[]>} a list of known session ids for the device
|
* @return {Promise<string[]>} a list of known session ids for the device
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||||
|
const log = logger.withPrefix("[getSessionIdsForDevice]");
|
||||||
|
|
||||||
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
||||||
logger.log("waiting for olm session to be created");
|
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
|
||||||
try {
|
try {
|
||||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -638,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
log,
|
||||||
);
|
);
|
||||||
|
|
||||||
return sessionIds;
|
return sessionIds;
|
||||||
@@ -651,13 +656,14 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
|||||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||||
* This should only be set to true of the calling function is the function
|
* This should only be set to true of the calling function is the function
|
||||||
* that marked the session as being in-progress.
|
* that marked the session as being in-progress.
|
||||||
|
* @param {Logger} [log] A possibly customised log
|
||||||
* @return {Promise<?string>} session id, or null if no established session
|
* @return {Promise<?string>} session id, or null if no established session
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype.getSessionIdForDevice = async function(
|
OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||||
theirDeviceIdentityKey, nowait,
|
theirDeviceIdentityKey, nowait, log,
|
||||||
) {
|
) {
|
||||||
const sessionInfos = await this.getSessionInfoForDevice(
|
const sessionInfos = await this.getSessionInfoForDevice(
|
||||||
theirDeviceIdentityKey, nowait,
|
theirDeviceIdentityKey, nowait, log,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sessionInfos.length === 0) {
|
if (sessionInfos.length === 0) {
|
||||||
@@ -697,11 +703,16 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
|
|||||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||||
* This should only be set to true of the calling function is the function
|
* This should only be set to true of the calling function is the function
|
||||||
* that marked the session as being in-progress.
|
* that marked the session as being in-progress.
|
||||||
|
* @param {Logger} [log] A possibly customised log
|
||||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
|
OlmDevice.prototype.getSessionInfoForDevice = async function(
|
||||||
|
deviceIdentityKey, nowait, log = logger,
|
||||||
|
) {
|
||||||
|
log = log.withPrefix("[getSessionInfoForDevice]");
|
||||||
|
|
||||||
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||||
logger.log("waiting for olm session to be created");
|
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
|
||||||
try {
|
try {
|
||||||
await this._sessionsInProgress[deviceIdentityKey];
|
await this._sessionsInProgress[deviceIdentityKey];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -727,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
log,
|
||||||
);
|
);
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@@ -761,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
|
|||||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[encryptMessage]"),
|
||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
@@ -794,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
|
|||||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[decryptMessage]"),
|
||||||
);
|
);
|
||||||
return payloadString;
|
return payloadString;
|
||||||
};
|
};
|
||||||
@@ -825,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
|
|||||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[matchesSession]"),
|
||||||
);
|
);
|
||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
@@ -1095,6 +1110,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[addInboundGroupSession]"),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1265,6 +1281,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[decryptGroupMessage]"),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -1310,6 +1327,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -1369,6 +1387,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
logger.withPrefix("[getInboundGroupSessionKey]"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ limitations under the License.
|
|||||||
* @module crypto/algorithms/megolm
|
* @module crypto/algorithms/megolm
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {getPrefixedLogger, logger} from '../../logger';
|
import {logger} from '../../logger';
|
||||||
import * as utils from "../../utils";
|
import * as utils from "../../utils";
|
||||||
import {polyfillSuper} from "../../utils";
|
import {polyfillSuper} from "../../utils";
|
||||||
import * as olmlib from "../olmlib";
|
import * as olmlib from "../olmlib";
|
||||||
@@ -736,7 +736,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(
|
|||||||
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
||||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||||
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
||||||
getPrefixedLogger(`[${this._roomId}]`),
|
logger.withPrefix(`[${this._roomId}]`),
|
||||||
);
|
);
|
||||||
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export async function getExistingOlmSessions(
|
|||||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||||
* failed to respond to one-time-key requests.
|
* failed to respond to one-time-key requests.
|
||||||
*
|
*
|
||||||
* @param {Object} [log] A possibly customised log
|
* @param {Logger} [log] A possibly customised log
|
||||||
*
|
*
|
||||||
* @return {Promise} resolves once the sessions are complete, to
|
* @return {Promise} resolves once the sessions are complete, to
|
||||||
* an Object mapping from userId to deviceId to
|
* an Object mapping from userId to deviceId to
|
||||||
@@ -208,6 +208,39 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
const result = {};
|
const result = {};
|
||||||
const resolveSession = {};
|
const resolveSession = {};
|
||||||
|
|
||||||
|
// Mark all sessions this task intends to update as in progress. It is
|
||||||
|
// important to do this for all devices this task cares about in a single
|
||||||
|
// synchronous operation, as otherwise it is possible to have deadlocks
|
||||||
|
// where multiple tasks wait indefinitely on another task to update some set
|
||||||
|
// of common devices.
|
||||||
|
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||||
|
for (const deviceInfo of devices) {
|
||||||
|
const deviceId = deviceInfo.deviceId;
|
||||||
|
const key = deviceInfo.getIdentityKey();
|
||||||
|
|
||||||
|
if (key === olmDevice.deviceCurve25519Key) {
|
||||||
|
// We don't start sessions with ourself, so there's no need to
|
||||||
|
// mark it in progress.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||||
|
if (!olmDevice._sessionsInProgress[key]) {
|
||||||
|
// pre-emptively mark the session as in-progress to avoid race
|
||||||
|
// conditions. If we find that we already have a session, then
|
||||||
|
// we'll resolve
|
||||||
|
log.debug(`Marking Olm session in progress ${forWhom}`);
|
||||||
|
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||||
|
resolveSession[key] = (...args) => {
|
||||||
|
log.debug(`Resolved Olm session in progress ${forWhom}`);
|
||||||
|
delete olmDevice._sessionsInProgress[key];
|
||||||
|
resolve(...args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||||
result[userId] = {};
|
result[userId] = {};
|
||||||
for (const deviceInfo of devices) {
|
for (const deviceInfo of devices) {
|
||||||
@@ -232,41 +265,23 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!olmDevice._sessionsInProgress[key]) {
|
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||||
// pre-emptively mark the session as in-progress to avoid race
|
log.debug(`Ensuring Olm session ${forWhom}`);
|
||||||
// conditions. If we find that we already have a session, then
|
|
||||||
// we'll resolve
|
|
||||||
olmDevice._sessionsInProgress[key] = new Promise(
|
|
||||||
(resolve, reject) => {
|
|
||||||
resolveSession[key] = {
|
|
||||||
resolve: (...args) => {
|
|
||||||
delete olmDevice._sessionsInProgress[key];
|
|
||||||
resolve(...args);
|
|
||||||
},
|
|
||||||
reject: (...args) => {
|
|
||||||
delete olmDevice._sessionsInProgress[key];
|
|
||||||
reject(...args);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||||
key, resolveSession[key],
|
key, resolveSession[key], log,
|
||||||
);
|
);
|
||||||
|
log.debug(`Got Olm session ${sessionId} ${forWhom}`);
|
||||||
if (sessionId !== null && resolveSession[key]) {
|
if (sessionId !== null && resolveSession[key]) {
|
||||||
// we found a session, but we had marked the session as
|
// we found a session, but we had marked the session as
|
||||||
// in-progress, so unmark it and unblock anything that was
|
// in-progress, so resolve it now, which will unmark it and
|
||||||
// waiting
|
// unblock anything that was waiting
|
||||||
delete olmDevice._sessionsInProgress[key];
|
resolveSession[key]();
|
||||||
resolveSession[key].resolve();
|
|
||||||
delete resolveSession[key];
|
|
||||||
}
|
}
|
||||||
if (sessionId === null || force) {
|
if (sessionId === null || force) {
|
||||||
if (force) {
|
if (force) {
|
||||||
log.info(`Forcing new Olm session for ${userId}:${deviceId}`);
|
log.info(`Forcing new Olm session ${forWhom}`);
|
||||||
} else {
|
} else {
|
||||||
log.info(`Making new Olm session for ${userId}:${deviceId}`);
|
log.info(`Making new Olm session ${forWhom}`);
|
||||||
}
|
}
|
||||||
devicesWithoutSession.push([userId, deviceId]);
|
devicesWithoutSession.push([userId, deviceId]);
|
||||||
}
|
}
|
||||||
@@ -289,9 +304,13 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
// timeout on this request, let's first log whether that's the root
|
// timeout on this request, let's first log whether that's the root
|
||||||
// cause we're seeing in practice.
|
// cause we're seeing in practice.
|
||||||
// See also https://github.com/vector-im/element-web/issues/16194
|
// See also https://github.com/vector-im/element-web/issues/16194
|
||||||
const otkTimeoutLogger = setTimeout(() => {
|
let otkTimeoutLogger;
|
||||||
|
// XXX: Perhaps there should be a default timeout?
|
||||||
|
if (otkTimeout) {
|
||||||
|
otkTimeoutLogger = setTimeout(() => {
|
||||||
log.error(`Homeserver never replied while claiming ${taskDetail}`);
|
log.error(`Homeserver never replied while claiming ${taskDetail}`);
|
||||||
}, otkTimeout);
|
}, otkTimeout);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
log.debug(`Claiming ${taskDetail}`);
|
log.debug(`Claiming ${taskDetail}`);
|
||||||
res = await baseApis.claimOneTimeKeys(
|
res = await baseApis.claimOneTimeKeys(
|
||||||
@@ -300,7 +319,7 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
log.debug(`Claimed ${taskDetail}`);
|
log.debug(`Claimed ${taskDetail}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
for (const resolver of Object.values(resolveSession)) {
|
for (const resolver of Object.values(resolveSession)) {
|
||||||
resolver.resolve();
|
resolver();
|
||||||
}
|
}
|
||||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||||
throw e;
|
throw e;
|
||||||
@@ -312,10 +331,10 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
failedServers.push(...Object.keys(res.failures));
|
failedServers.push(...Object.keys(res.failures));
|
||||||
}
|
}
|
||||||
|
|
||||||
const otk_res = res.one_time_keys || {};
|
const otkResult = res.one_time_keys || {};
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||||
const userRes = otk_res[userId] || {};
|
const userRes = otkResult[userId] || {};
|
||||||
for (let j = 0; j < devices.length; j++) {
|
for (let j = 0; j < devices.length; j++) {
|
||||||
const deviceInfo = devices[j];
|
const deviceInfo = devices[j];
|
||||||
const deviceId = deviceInfo.deviceId;
|
const deviceId = deviceInfo.deviceId;
|
||||||
@@ -347,7 +366,7 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
`for device ${userId}:${deviceId}`,
|
`for device ${userId}:${deviceId}`,
|
||||||
);
|
);
|
||||||
if (resolveSession[key]) {
|
if (resolveSession[key]) {
|
||||||
resolveSession[key].resolve();
|
resolveSession[key]();
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -357,12 +376,12 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
olmDevice, oneTimeKey, userId, deviceInfo,
|
olmDevice, oneTimeKey, userId, deviceInfo,
|
||||||
).then((sid) => {
|
).then((sid) => {
|
||||||
if (resolveSession[key]) {
|
if (resolveSession[key]) {
|
||||||
resolveSession[key].resolve(sid);
|
resolveSession[key](sid);
|
||||||
}
|
}
|
||||||
result[userId][deviceId].sessionId = sid;
|
result[userId][deviceId].sessionId = sid;
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
if (resolveSession[key]) {
|
if (resolveSession[key]) {
|
||||||
resolveSession[key].resolve();
|
resolveSession[key]();
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class Backend {
|
|||||||
*/
|
*/
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
this._db = db;
|
this._db = db;
|
||||||
|
this._nextTxnId = 0;
|
||||||
|
|
||||||
// make sure we close the db on `onversionchange` - otherwise
|
// make sure we close the db on `onversionchange` - otherwise
|
||||||
// attempts to delete the database will block (and subsequent
|
// attempts to delete the database will block (and subsequent
|
||||||
@@ -757,10 +758,21 @@ export class Backend {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
doTxn(mode, stores, func) {
|
doTxn(mode, stores, func, log = logger) {
|
||||||
|
const txnId = this._nextTxnId++;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const description = `${mode} crypto store transaction ${txnId} in ${stores}`;
|
||||||
|
log.debug(`Starting ${description}`);
|
||||||
const txn = this._db.transaction(stores, mode);
|
const txn = this._db.transaction(stores, mode);
|
||||||
const promise = promiseifyTxn(txn);
|
const promise = promiseifyTxn(txn);
|
||||||
const result = func(txn);
|
const result = func(txn);
|
||||||
|
promise.then(() => {
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
|
||||||
|
}, () => {
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
log.error(`Failed ${description}, took ${elapsedTime} ms`);
|
||||||
|
});
|
||||||
return promise.then(() => {
|
return promise.then(() => {
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ export class IndexedDBCryptoStore {
|
|||||||
* @param {function(*)} func Function called with the
|
* @param {function(*)} func Function called with the
|
||||||
* transaction object: an opaque object that should be passed
|
* transaction object: an opaque object that should be passed
|
||||||
* to store functions.
|
* to store functions.
|
||||||
|
* @param {Logger} [log] A possibly customised log
|
||||||
* @return {Promise} Promise that resolves with the result of the `func`
|
* @return {Promise} Promise that resolves with the result of the `func`
|
||||||
* when the transaction is complete. If the backend is
|
* when the transaction is complete. If the backend is
|
||||||
* async (ie. the indexeddb backend) any of the callback
|
* async (ie. the indexeddb backend) any of the callback
|
||||||
@@ -603,8 +604,8 @@ export class IndexedDBCryptoStore {
|
|||||||
* reject with that exception. On synchronous backends, the
|
* reject with that exception. On synchronous backends, the
|
||||||
* exception will propagate to the caller of the getFoo method.
|
* exception will propagate to the caller of the getFoo method.
|
||||||
*/
|
*/
|
||||||
doTxn(mode, stores, func) {
|
doTxn(mode, stores, func, log) {
|
||||||
return this._backend.doTxn(mode, stores, func);
|
return this._backend.doTxn(mode, stores, func, log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,17 +59,28 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
|
|||||||
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
|
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
|
||||||
* Can be tailored down to specific use cases if needed.
|
* Can be tailored down to specific use cases if needed.
|
||||||
*/
|
*/
|
||||||
export const logger = log.getLogger(DEFAULT_NAMESPACE);
|
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
|
||||||
logger.setLevel(log.levels.DEBUG);
|
logger.setLevel(log.levels.DEBUG);
|
||||||
|
|
||||||
interface PrefixedLogger extends Logger {
|
interface PrefixedLogger extends Logger {
|
||||||
prefix?: any;
|
withPrefix?: (prefix: string) => PrefixedLogger;
|
||||||
|
prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrefixedLogger(prefix): PrefixedLogger {
|
function extendLogger(logger: PrefixedLogger) {
|
||||||
|
logger.withPrefix = function(prefix: string): PrefixedLogger {
|
||||||
|
const existingPrefix = this.prefix || "";
|
||||||
|
return getPrefixedLogger(existingPrefix + prefix);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extendLogger(logger);
|
||||||
|
|
||||||
|
function getPrefixedLogger(prefix): PrefixedLogger {
|
||||||
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
|
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
|
||||||
if (prefixLogger.prefix !== prefix) {
|
if (prefixLogger.prefix !== prefix) {
|
||||||
// Only do this setup work the first time through, as loggers are saved by name.
|
// Only do this setup work the first time through, as loggers are saved by name.
|
||||||
|
extendLogger(prefixLogger);
|
||||||
prefixLogger.prefix = prefix;
|
prefixLogger.prefix = prefix;
|
||||||
prefixLogger.setLevel(log.levels.DEBUG);
|
prefixLogger.setLevel(log.levels.DEBUG);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export * from "./store/session/webstorage";
|
|||||||
export * from "./crypto/store/memory-crypto-store";
|
export * from "./crypto/store/memory-crypto-store";
|
||||||
export * from "./crypto/store/indexeddb-crypto-store";
|
export * from "./crypto/store/indexeddb-crypto-store";
|
||||||
export * from "./content-repo";
|
export * from "./content-repo";
|
||||||
export const ContentHelpers = import("./content-helpers");
|
export * as ContentHelpers from "./content-helpers";
|
||||||
export {
|
export {
|
||||||
createNewMatrixCall,
|
createNewMatrixCall,
|
||||||
setAudioOutput as setMatrixCallAudioOutput,
|
setAudioOutput as setMatrixCallAudioOutput,
|
||||||
|
|||||||
@@ -290,6 +290,9 @@ RoomMember.prototype.getMxcAvatarUrl = function() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MXID_PATTERN = /@.+:.+/;
|
||||||
|
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||||
|
|
||||||
function calculateDisplayName(selfUserId, displayName, roomState) {
|
function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||||
if (!displayName || displayName === selfUserId) {
|
if (!displayName || displayName === selfUserId) {
|
||||||
return selfUserId;
|
return selfUserId;
|
||||||
@@ -308,13 +311,13 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
|
|||||||
// Next check if the name contains something that look like a mxid
|
// Next check if the name contains something that look like a mxid
|
||||||
// If it does, it may be someone trying to impersonate someone else
|
// If it does, it may be someone trying to impersonate someone else
|
||||||
// Show full mxid in this case
|
// Show full mxid in this case
|
||||||
let disambiguate = /@.+:.+/.test(displayName);
|
let disambiguate = MXID_PATTERN.test(displayName);
|
||||||
|
|
||||||
if (!disambiguate) {
|
if (!disambiguate) {
|
||||||
// Also show mxid if the display name contains any LTR/RTL characters as these
|
// Also show mxid if the display name contains any LTR/RTL characters as these
|
||||||
// make it very difficult for us to find similar *looking* display names
|
// make it very difficult for us to find similar *looking* display names
|
||||||
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
||||||
disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
|
disambiguate = LTR_RTL_PATTERN.test(displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disambiguate) {
|
if (!disambiguate) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {RoomMember} from "./room-member";
|
|||||||
import {RoomSummary} from "./room-summary";
|
import {RoomSummary} from "./room-summary";
|
||||||
import {logger} from '../logger';
|
import {logger} from '../logger';
|
||||||
import {ReEmitter} from '../ReEmitter';
|
import {ReEmitter} from '../ReEmitter';
|
||||||
import {EventType} from "../@types/event";
|
import {EventType, RoomCreateTypeField, RoomType} from "../@types/event";
|
||||||
|
|
||||||
// These constants are used as sane defaults when the homeserver doesn't support
|
// These constants are used as sane defaults when the homeserver doesn't support
|
||||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||||
@@ -1856,6 +1856,27 @@ Room.prototype.getJoinRule = function() {
|
|||||||
return this.currentState.getJoinRule();
|
return this.currentState.getJoinRule();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
|
||||||
|
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
|
||||||
|
*/
|
||||||
|
Room.prototype.getType = function() {
|
||||||
|
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||||
|
if (!createEvent) {
|
||||||
|
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return createEvent.getContent()[RoomCreateTypeField];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the room is a space-room as defined by MSC1772.
|
||||||
|
* @returns {boolean} true if the room's type is RoomType.Space
|
||||||
|
*/
|
||||||
|
Room.prototype.isSpaceRoom = function() {
|
||||||
|
return this.getType() === RoomType.Space;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an internal method. Calculates the name of the room from the current
|
* This is an internal method. Calculates the name of the room from the current
|
||||||
* room state.
|
* room state.
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
|||||||
* sync from the server is not required. This does not reduce memory usage as all
|
* sync from the server is not required. This does not reduce memory usage as all
|
||||||
* the data is eagerly fetched when <code>startup()</code> is called.
|
* the data is eagerly fetched when <code>startup()</code> is called.
|
||||||
* <pre>
|
* <pre>
|
||||||
* let opts = { localStorage: window.localStorage };
|
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
||||||
* let store = new IndexedDBStore();
|
* let store = new IndexedDBStore(opts);
|
||||||
* await store.startup(); // load from indexed db
|
* await store.startup(); // load from indexed db
|
||||||
* let client = sdk.createClient({
|
* let client = sdk.createClient({
|
||||||
* store: store,
|
* store: store,
|
||||||
|
|||||||
@@ -174,6 +174,11 @@ export enum CallErrorCode {
|
|||||||
SignallingFailed = 'signalling_timeout',
|
SignallingFailed = 'signalling_timeout',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ConstraintsType {
|
||||||
|
Audio = "audio",
|
||||||
|
Video = "video",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version field that we set in m.call.* events
|
* The version field that we set in m.call.* events
|
||||||
*/
|
*/
|
||||||
@@ -251,8 +256,6 @@ export class MatrixCall extends EventEmitter {
|
|||||||
private localAVStream: MediaStream;
|
private localAVStream: MediaStream;
|
||||||
private inviteOrAnswerSent: boolean;
|
private inviteOrAnswerSent: boolean;
|
||||||
private waitForLocalAVStream: boolean;
|
private waitForLocalAVStream: boolean;
|
||||||
// XXX: This is either the invite or answer from remote...
|
|
||||||
private msg: any;
|
|
||||||
// XXX: I don't know why this is called 'config'.
|
// XXX: I don't know why this is called 'config'.
|
||||||
private config: MediaStreamConstraints;
|
private config: MediaStreamConstraints;
|
||||||
private successor: MatrixCall;
|
private successor: MatrixCall;
|
||||||
@@ -284,6 +287,11 @@ export class MatrixCall extends EventEmitter {
|
|||||||
private makingOffer: boolean;
|
private makingOffer: boolean;
|
||||||
private ignoreOffer: boolean;
|
private ignoreOffer: boolean;
|
||||||
|
|
||||||
|
// If candidates arrive before we've picked an opponent (which, in particular,
|
||||||
|
// will happen if the opponent sends candidates eagerly before the user answers
|
||||||
|
// the call) we buffer them up here so we can then add the ones from the party we pick
|
||||||
|
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
|
||||||
|
|
||||||
constructor(opts: CallOpts) {
|
constructor(opts: CallOpts) {
|
||||||
super();
|
super();
|
||||||
this.roomId = opts.roomId;
|
this.roomId = opts.roomId;
|
||||||
@@ -291,9 +299,6 @@ export class MatrixCall extends EventEmitter {
|
|||||||
this.type = null;
|
this.type = null;
|
||||||
this.forceTURN = opts.forceTURN;
|
this.forceTURN = opts.forceTURN;
|
||||||
this.ourPartyId = this.client.deviceId;
|
this.ourPartyId = this.client.deviceId;
|
||||||
// We compare this to null to checks the presence of a party ID:
|
|
||||||
// make sure it's null, not undefined
|
|
||||||
this.opponentPartyId = null;
|
|
||||||
// Array of Objects with urls, username, credential keys
|
// Array of Objects with urls, username, credential keys
|
||||||
this.turnServers = opts.turnServers || [];
|
this.turnServers = opts.turnServers || [];
|
||||||
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
||||||
@@ -331,7 +336,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
placeVoiceCall() {
|
placeVoiceCall() {
|
||||||
logger.debug("placeVoiceCall");
|
logger.debug("placeVoiceCall");
|
||||||
this.checkForErrorListener();
|
this.checkForErrorListener();
|
||||||
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Voice));
|
const constraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||||
|
this.placeCallWithConstraints(constraints);
|
||||||
this.type = CallType.Voice;
|
this.type = CallType.Voice;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +354,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
this.checkForErrorListener();
|
this.checkForErrorListener();
|
||||||
this.localVideoElement = localVideoElement;
|
this.localVideoElement = localVideoElement;
|
||||||
this.remoteVideoElement = remoteVideoElement;
|
this.remoteVideoElement = remoteVideoElement;
|
||||||
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Video));
|
const constraints = getUserMediaContraints(ConstraintsType.Video);
|
||||||
|
this.placeCallWithConstraints(constraints);
|
||||||
this.type = CallType.Video;
|
this.type = CallType.Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,43 +379,21 @@ export class MatrixCall extends EventEmitter {
|
|||||||
this.localVideoElement = localVideoElement;
|
this.localVideoElement = localVideoElement;
|
||||||
this.remoteVideoElement = remoteVideoElement;
|
this.remoteVideoElement = remoteVideoElement;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
|
||||||
|
if (!screenshareConstraints) return;
|
||||||
if (window.electron?.getDesktopCapturerSources) {
|
if (window.electron?.getDesktopCapturerSources) {
|
||||||
// We have access to getDesktopCapturerSources()
|
// We are using Electron
|
||||||
logger.debug("Electron getDesktopCapturerSources() is available...");
|
logger.debug("Getting screen stream using getUserMedia()...");
|
||||||
try {
|
this.screenSharingStream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
|
||||||
const selectedSource = await selectDesktopCapturerSource();
|
|
||||||
// If no source was selected cancel call
|
|
||||||
if (!selectedSource) return;
|
|
||||||
const getUserMediaOptions: MediaStreamConstraints | DesktopCapturerConstraints = {
|
|
||||||
audio: false,
|
|
||||||
video: {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: "desktop",
|
|
||||||
chromeMediaSourceId: selectedSource.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.screenSharingStream = await window.navigator.mediaDevices.getUserMedia(getUserMediaOptions);
|
|
||||||
|
|
||||||
logger.debug("Got screen stream, requesting audio stream...");
|
|
||||||
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
|
|
||||||
this.placeCallWithConstraints(audioConstraints);
|
|
||||||
} catch (err) {
|
|
||||||
this.emit(CallEvent.Error,
|
|
||||||
new CallError(
|
|
||||||
CallErrorCode.NoUserMedia,
|
|
||||||
"Failed to get screen-sharing stream: ", err,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
/* We do not have access to the Electron desktop capturer,
|
// We are not using Electron
|
||||||
* therefore we can assume we are on the web */
|
logger.debug("Getting screen stream using getDisplayMedia()...");
|
||||||
logger.debug("Electron desktopCapturer is not available...");
|
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
|
||||||
try {
|
}
|
||||||
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
|
|
||||||
logger.debug("Got screen stream, requesting audio stream...");
|
logger.debug("Got screen stream, requesting audio stream...");
|
||||||
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
|
const audioConstraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||||
this.placeCallWithConstraints(audioConstraints);
|
this.placeCallWithConstraints(audioConstraints);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.emit(CallEvent.Error,
|
this.emit(CallEvent.Error,
|
||||||
@@ -418,8 +403,6 @@ export class MatrixCall extends EventEmitter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.type = CallType.Video;
|
this.type = CallType.Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,11 +524,16 @@ export class MatrixCall extends EventEmitter {
|
|||||||
* @param {MatrixEvent} event The m.call.invite event
|
* @param {MatrixEvent} event The m.call.invite event
|
||||||
*/
|
*/
|
||||||
async initWithInvite(event: MatrixEvent) {
|
async initWithInvite(event: MatrixEvent) {
|
||||||
this.msg = event.getContent();
|
const invite = event.getContent();
|
||||||
this.direction = CallDirection.Inbound;
|
this.direction = CallDirection.Inbound;
|
||||||
|
|
||||||
this.peerConn = this.createPeerConnection();
|
this.peerConn = this.createPeerConnection();
|
||||||
|
// we must set the party ID before await-ing on anything: the call event
|
||||||
|
// handler will start giving us more call events (eg. candidates) so if
|
||||||
|
// we haven't set the party ID, we'll ignore them.
|
||||||
|
this.chooseOpponent(event);
|
||||||
try {
|
try {
|
||||||
await this.peerConn.setRemoteDescription(this.msg.offer);
|
await this.peerConn.setRemoteDescription(invite.offer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug("Failed to set remote description", e);
|
logger.debug("Failed to set remote description", e);
|
||||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||||
@@ -564,13 +552,6 @@ export class MatrixCall extends EventEmitter {
|
|||||||
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||||
|
|
||||||
this.setState(CallState.Ringing);
|
this.setState(CallState.Ringing);
|
||||||
this.opponentVersion = this.msg.version;
|
|
||||||
if (this.opponentVersion !== 0) {
|
|
||||||
// ignore party ID in v0 calls: party ID isn't a thing until v1
|
|
||||||
this.opponentPartyId = this.msg.party_id || null;
|
|
||||||
}
|
|
||||||
this.opponentCaps = this.msg.capabilities || {};
|
|
||||||
this.opponentMember = event.sender;
|
|
||||||
|
|
||||||
if (event.getLocalAge()) {
|
if (event.getLocalAge()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -584,7 +565,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit(CallEvent.Hangup);
|
this.emit(CallEvent.Hangup);
|
||||||
}
|
}
|
||||||
}, this.msg.lifetime - event.getLocalAge());
|
}, invite.lifetime - event.getLocalAge());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +577,6 @@ export class MatrixCall extends EventEmitter {
|
|||||||
// perverse as it may seem, sometimes we want to instantiate a call with a
|
// perverse as it may seem, sometimes we want to instantiate a call with a
|
||||||
// hangup message (because when getting the state of the room on load, events
|
// hangup message (because when getting the state of the room on load, events
|
||||||
// come in reverse order and we want to remember that a call has been hung up)
|
// come in reverse order and we want to remember that a call has been hung up)
|
||||||
this.msg = event.getContent();
|
|
||||||
this.setState(CallState.Ended);
|
this.setState(CallState.Ended);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +591,11 @@ export class MatrixCall extends EventEmitter {
|
|||||||
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
|
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
|
||||||
|
|
||||||
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
||||||
const constraints = getUserMediaVideoContraints(this.type);
|
const constraints = getUserMediaContraints(
|
||||||
|
this.type == CallType.Video ?
|
||||||
|
ConstraintsType.Video:
|
||||||
|
ConstraintsType.Audio,
|
||||||
|
);
|
||||||
logger.log("Getting user media with constraints", constraints);
|
logger.log("Getting user media with constraints", constraints);
|
||||||
this.setState(CallState.WaitLocalMedia);
|
this.setState(CallState.WaitLocalMedia);
|
||||||
this.waitForLocalAVStream = true;
|
this.waitForLocalAVStream = true;
|
||||||
@@ -886,7 +870,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
// Now we wait for the negotiationneeded event
|
// Now we wait for the negotiationneeded event
|
||||||
};
|
};
|
||||||
|
|
||||||
private sendAnswer() {
|
private async sendAnswer() {
|
||||||
const answerContent = {
|
const answerContent = {
|
||||||
answer: {
|
answer: {
|
||||||
sdp: this.peerConn.localDescription.sdp,
|
sdp: this.peerConn.localDescription.sdp,
|
||||||
@@ -908,12 +892,12 @@ export class MatrixCall extends EventEmitter {
|
|||||||
logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`);
|
logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`);
|
||||||
this.candidateSendQueue = [];
|
this.candidateSendQueue = [];
|
||||||
|
|
||||||
this.sendVoipEvent(EventType.CallAnswer, answerContent).then(() => {
|
try {
|
||||||
|
await this.sendVoipEvent(EventType.CallAnswer, answerContent);
|
||||||
// If this isn't the first time we've tried to send the answer,
|
// If this isn't the first time we've tried to send the answer,
|
||||||
// we may have candidates queued up, so send them now.
|
// we may have candidates queued up, so send them now.
|
||||||
this.inviteOrAnswerSent = true;
|
this.inviteOrAnswerSent = true;
|
||||||
this.sendCandidateQueue();
|
} catch (error) {
|
||||||
}).catch((error) => {
|
|
||||||
// We've failed to answer: back to the ringing state
|
// We've failed to answer: back to the ringing state
|
||||||
this.setState(CallState.Ringing);
|
this.setState(CallState.Ringing);
|
||||||
this.client.cancelPendingEvent(error.event);
|
this.client.cancelPendingEvent(error.event);
|
||||||
@@ -926,7 +910,11 @@ export class MatrixCall extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit(CallEvent.Error, new CallError(code, message, error));
|
this.emit(CallEvent.Error, new CallError(code, message, error));
|
||||||
throw error;
|
throw error;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// error handler re-throws so this won't happen on error, but
|
||||||
|
// we don't want the same error handling on the candidate queue
|
||||||
|
this.sendCandidateQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private gotUserMediaForAnswer = async (stream: MediaStream) => {
|
private gotUserMediaForAnswer = async (stream: MediaStream) => {
|
||||||
@@ -1030,37 +1018,33 @@ export class MatrixCall extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.partyIdMatches(ev.getContent())) {
|
|
||||||
logger.info(
|
|
||||||
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
|
|
||||||
`we have chosen party ID ${this.opponentPartyId}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cands = ev.getContent().candidates;
|
const cands = ev.getContent().candidates;
|
||||||
if (!cands) {
|
if (!cands) {
|
||||||
logger.info("Ignoring candidates event with no candidates!");
|
logger.info("Ignoring candidates event with no candidates!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cand of cands) {
|
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
|
||||||
if (
|
|
||||||
(cand.sdpMid === null || cand.sdpMid === undefined) &&
|
if (this.opponentPartyId === undefined) {
|
||||||
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
|
// we haven't picked an opponent yet so save the candidates
|
||||||
) {
|
logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
|
||||||
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
|
const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
|
||||||
|
bufferedCands.push(...cands);
|
||||||
|
this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
|
||||||
try {
|
if (!this.partyIdMatches(ev.getContent())) {
|
||||||
this.peerConn.addIceCandidate(cand);
|
logger.info(
|
||||||
} catch (err) {
|
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
|
||||||
if (!this.ignoreOffer) {
|
`we have chosen party ID ${this.opponentPartyId}`,
|
||||||
logger.info("Failed to add remore ICE candidate", err);
|
);
|
||||||
}
|
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addIceCandidates(cands);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1072,7 +1056,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.opponentPartyId !== null) {
|
if (this.opponentPartyId !== undefined) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
|
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
|
||||||
`we already have an answer/reject from ${this.opponentPartyId}`,
|
`we already have an answer/reject from ${this.opponentPartyId}`,
|
||||||
@@ -1080,12 +1064,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.opponentVersion = event.getContent().version;
|
this.chooseOpponent(event);
|
||||||
if (this.opponentVersion !== 0) {
|
|
||||||
this.opponentPartyId = event.getContent().party_id || null;
|
|
||||||
}
|
|
||||||
this.opponentCaps = event.getContent().capabilities || {};
|
|
||||||
this.opponentMember = event.sender;
|
|
||||||
|
|
||||||
this.setState(CallState.Connecting);
|
this.setState(CallState.Connecting);
|
||||||
|
|
||||||
@@ -1260,19 +1239,9 @@ export class MatrixCall extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.sendVoipEvent(eventType, content);
|
await this.sendVoipEvent(eventType, content);
|
||||||
this.sendCandidateQueue();
|
|
||||||
if (this.state === CallState.CreateOffer) {
|
|
||||||
this.inviteOrAnswerSent = true;
|
|
||||||
this.setState(CallState.InviteSent);
|
|
||||||
this.inviteTimeout = setTimeout(() => {
|
|
||||||
this.inviteTimeout = null;
|
|
||||||
if (this.state === CallState.InviteSent) {
|
|
||||||
this.hangup(CallErrorCode.InviteTimeout, false);
|
|
||||||
}
|
|
||||||
}, CALL_TIMEOUT_MS);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.client.cancelPendingEvent(error.event);
|
logger.error("Failed to send invite", error);
|
||||||
|
if (error.event) this.client.cancelPendingEvent(error.event);
|
||||||
|
|
||||||
let code = CallErrorCode.SignallingFailed;
|
let code = CallErrorCode.SignallingFailed;
|
||||||
let message = "Signalling failed";
|
let message = "Signalling failed";
|
||||||
@@ -1287,6 +1256,22 @@ export class MatrixCall extends EventEmitter {
|
|||||||
|
|
||||||
this.emit(CallEvent.Error, new CallError(code, message, error));
|
this.emit(CallEvent.Error, new CallError(code, message, error));
|
||||||
this.terminate(CallParty.Local, code, false);
|
this.terminate(CallParty.Local, code, false);
|
||||||
|
|
||||||
|
// no need to carry on & send the candidate queue, but we also
|
||||||
|
// don't want to rethrow the error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
if (this.state === CallState.CreateOffer) {
|
||||||
|
this.inviteOrAnswerSent = true;
|
||||||
|
this.setState(CallState.InviteSent);
|
||||||
|
this.inviteTimeout = setTimeout(() => {
|
||||||
|
this.inviteTimeout = null;
|
||||||
|
if (this.state === CallState.InviteSent) {
|
||||||
|
this.hangup(CallErrorCode.InviteTimeout, false);
|
||||||
|
}
|
||||||
|
}, CALL_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1623,7 +1608,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendCandidateQueue() {
|
private async sendCandidateQueue() {
|
||||||
if (this.candidateSendQueue.length === 0) {
|
if (this.candidateSendQueue.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1635,20 +1620,28 @@ export class MatrixCall extends EventEmitter {
|
|||||||
candidates: cands,
|
candidates: cands,
|
||||||
};
|
};
|
||||||
logger.debug("Attempting to send " + cands.length + " candidates");
|
logger.debug("Attempting to send " + cands.length + " candidates");
|
||||||
this.sendVoipEvent(EventType.CallCandidates, content).then(() => {
|
try {
|
||||||
this.candidateSendTries = 0;
|
await this.sendVoipEvent(EventType.CallCandidates, content);
|
||||||
this.sendCandidateQueue();
|
} catch (error) {
|
||||||
}, (error) => {
|
// don't retry this event: we'll send another one later as we might
|
||||||
for (let i = 0; i < cands.length; i++) {
|
// have more candidates by then.
|
||||||
this.candidateSendQueue.push(cands[i]);
|
if (error.event) this.client.cancelPendingEvent(error.event);
|
||||||
}
|
|
||||||
|
// put all the candidates we failed to send back in the queue
|
||||||
|
this.candidateSendQueue.push(...cands);
|
||||||
|
|
||||||
if (this.candidateSendTries > 5) {
|
if (this.candidateSendTries > 5) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Failed to send candidates on attempt " + this.candidateSendTries +
|
"Failed to send candidates on attempt " + this.candidateSendTries +
|
||||||
". Giving up for now.", error,
|
". Giving up on this call.", error,
|
||||||
);
|
);
|
||||||
this.candidateSendTries = 0;
|
|
||||||
|
const code = CallErrorCode.SignallingFailed;
|
||||||
|
const message = "Signalling failed";
|
||||||
|
|
||||||
|
this.emit(CallEvent.Error, new CallError(code, message, error));
|
||||||
|
this.hangup(code, false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1658,7 +1651,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.sendCandidateQueue();
|
this.sendCandidateQueue();
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
|
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
|
||||||
@@ -1702,9 +1695,60 @@ export class MatrixCall extends EventEmitter {
|
|||||||
|
|
||||||
private partyIdMatches(msg): boolean {
|
private partyIdMatches(msg): boolean {
|
||||||
// They must either match or both be absent (in which case opponentPartyId will be null)
|
// They must either match or both be absent (in which case opponentPartyId will be null)
|
||||||
const msgPartyId = msg.party_id || null;
|
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
|
||||||
|
// here and use null if the version is 0 (woe betide any opponent sending messages in the
|
||||||
|
// same call with different versions)
|
||||||
|
const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
|
||||||
return msgPartyId === this.opponentPartyId;
|
return msgPartyId === this.opponentPartyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commits to an opponent for the call
|
||||||
|
// ev: An invite or answer event
|
||||||
|
private chooseOpponent(ev: MatrixEvent) {
|
||||||
|
// I choo-choo-choose you
|
||||||
|
const msg = ev.getContent();
|
||||||
|
|
||||||
|
this.opponentVersion = msg.version;
|
||||||
|
if (this.opponentVersion === 0) {
|
||||||
|
// set to null to indicate that we've chosen an opponent, but because
|
||||||
|
// they're v0 they have no party ID (even if they sent one, we're ignoring it)
|
||||||
|
this.opponentPartyId = null;
|
||||||
|
} else {
|
||||||
|
// set to their party ID, or if they're naughty and didn't send one despite
|
||||||
|
// not being v0, set it to null to indicate we picked an opponent with no
|
||||||
|
// party ID
|
||||||
|
this.opponentPartyId = msg.party_id || null;
|
||||||
|
}
|
||||||
|
this.opponentCaps = msg.capabilities || {};
|
||||||
|
this.opponentMember = ev.sender;
|
||||||
|
|
||||||
|
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
|
||||||
|
if (bufferedCands) {
|
||||||
|
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
|
||||||
|
this.addIceCandidates(bufferedCands);
|
||||||
|
}
|
||||||
|
this.remoteCandidateBuffer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addIceCandidates(cands: RTCIceCandidate[]) {
|
||||||
|
for (const cand of cands) {
|
||||||
|
if (
|
||||||
|
(cand.sdpMid === null || cand.sdpMid === undefined) &&
|
||||||
|
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
|
||||||
|
) {
|
||||||
|
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
||||||
|
try {
|
||||||
|
this.peerConn.addIceCandidate(cand);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this.ignoreOffer) {
|
||||||
|
logger.info("Failed to add remore ICE candidate", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
|
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
|
||||||
@@ -1713,17 +1757,19 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserMediaVideoContraints(callType: CallType) {
|
function getUserMediaContraints(type: ConstraintsType) {
|
||||||
const isWebkit = !!navigator.webkitGetUserMedia;
|
const isWebkit = !!navigator.webkitGetUserMedia;
|
||||||
|
|
||||||
switch (callType) {
|
switch (type) {
|
||||||
case CallType.Voice:
|
case ConstraintsType.Audio: {
|
||||||
return {
|
return {
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||||
}, video: false,
|
},
|
||||||
|
video: false,
|
||||||
};
|
};
|
||||||
case CallType.Video:
|
}
|
||||||
|
case ConstraintsType.Video: {
|
||||||
return {
|
return {
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||||
@@ -1740,6 +1786,33 @@ function getUserMediaVideoContraints(callType: CallType) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScreenshareContraints(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
|
||||||
|
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
|
||||||
|
// We have access to getDesktopCapturerSources()
|
||||||
|
logger.debug("Electron getDesktopCapturerSources() is available...");
|
||||||
|
const selectedSource = await selectDesktopCapturerSource();
|
||||||
|
if (!selectedSource) return null;
|
||||||
|
return {
|
||||||
|
audio: false,
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: "desktop",
|
||||||
|
chromeMediaSourceId: selectedSource.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// We do not have access to the Electron desktop capturer,
|
||||||
|
// therefore we can assume we are on the web
|
||||||
|
logger.debug("Electron desktopCapturer is not available...");
|
||||||
|
return {
|
||||||
|
audio: false,
|
||||||
|
video: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let audioOutput: string;
|
let audioOutput: string;
|
||||||
let audioInput: string;
|
let audioInput: string;
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ export class CallEventHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
|
||||||
|
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
|
||||||
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
||||||
forceTURN: this.client._forceTURN,
|
forceTURN: this.client._forceTURN,
|
||||||
});
|
});
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@@ -5069,11 +5069,16 @@ lodash.sortby@^4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4:
|
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
|
||||||
version "4.17.20"
|
version "4.17.20"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|
||||||
|
lodash@^4.17.4:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
loglevel@^1.7.1:
|
loglevel@^1.7.1:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||||
@@ -5917,9 +5922,9 @@ pug-attrs@^2.0.4:
|
|||||||
pug-runtime "^2.0.5"
|
pug-runtime "^2.0.5"
|
||||||
|
|
||||||
pug-code-gen@^2.0.2:
|
pug-code-gen@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2"
|
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.3.tgz#122eb9ada9b5bf601705fe15aaa0a7d26bc134ab"
|
||||||
integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==
|
integrity sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==
|
||||||
dependencies:
|
dependencies:
|
||||||
constantinople "^3.1.2"
|
constantinople "^3.1.2"
|
||||||
doctypes "^1.1.0"
|
doctypes "^1.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user