Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae6e2cca27 | |||
| bd920eef1f | |||
| ed6d4e5f6c | |||
| accfa325b5 | |||
| b6ef8d95cd | |||
| c1144e3810 | |||
| 8663fd402b | |||
| f34052fd31 | |||
| 27d75a269f | |||
| d208a7fc5f | |||
| 702e16e3df | |||
| 12050b14f0 | |||
| 2d4a4f1736 | |||
| cd38fb9b4c | |||
| 7941b16ec4 | |||
| 3ff517e76e | |||
| 9559b26310 | |||
| 56ea4b8741 | |||
| 6dabfcda6f | |||
| 0b7b35f800 | |||
| 0ffdf7c0f1 | |||
| 13b6db8eb4 | |||
| 481acb2a1a | |||
| 683092140d | |||
| 1bb8c2d1a5 | |||
| 60fd3b0786 | |||
| cd4abc4e9b | |||
| e6a21cc487 | |||
| 6c5fc153bf | |||
| 07f15b41a2 | |||
| 8375638d76 | |||
| bed7543b46 | |||
| dc55236263 | |||
| 5f3427c5d1 | |||
| 66e5af185d | |||
| 0ff611e033 | |||
| 737cadaabc | |||
| 9fb2fbaeec | |||
| 51e817a3a2 | |||
| 59c93b59bf | |||
| c18ef051fc | |||
| 1ac5c9acbd | |||
| a034ca171e | |||
| 977682d37f | |||
| 0bafe263d7 | |||
| 1a8fced80e | |||
| 1c4d0b5e99 | |||
| 844a2b457c | |||
| ccf06f2216 | |||
| f630a9f297 | |||
| 2f71c93b53 | |||
| 92032a17a8 | |||
| e531456d42 | |||
| f0b2d2fe4d | |||
| 427500220d | |||
| 32e19ead74 | |||
| d11adb6f43 | |||
| f6155a50f6 | |||
| 4efee9445d | |||
| 20746a433f | |||
| 31dacc4206 | |||
| 88e5c59a85 | |||
| cf74920b36 | |||
| 972c900b58 | |||
| 12d5fd79f7 | |||
| a29f6979b2 | |||
| 3a7146c77b | |||
| b178d8f629 | |||
| 0c94ee62a3 | |||
| e7562898cd | |||
| fb73ab6878 | |||
| 38f978791d | |||
| 5dd60de57d | |||
| 5efbfc2dba | |||
| fcd1dbad89 | |||
| ad521bf4c2 | |||
| 8152fa44e0 | |||
| e217bf9e37 | |||
| bfad21f811 | |||
| 81e68abce3 | |||
| 7963bb352d | |||
| 14d3882059 | |||
| dede508e89 | |||
| ea39b69f65 | |||
| eafecd36bc | |||
| 198c9a2507 | |||
| d07563013b | |||
| 9e967832cd | |||
| bfe1987cd9 | |||
| 1045538f1f | |||
| fccf08edcf | |||
| f43fe366b5 | |||
| 0f75f2ef9c | |||
| 2cdc68f9c3 | |||
| 5849ea8e63 | |||
| 20afebf339 | |||
| 20eaba191e | |||
| ba58d3c544 | |||
| 8e0fc8d460 | |||
| 22713d8f89 |
@@ -1,3 +1,81 @@
|
||||
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
|
||||
|
||||
* Don't send m.call.hangup if m.call.invite wasn't sent either
|
||||
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
|
||||
* docs: registerGuest()
|
||||
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
|
||||
* Download device keys in chunks of 250
|
||||
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
|
||||
* More VoIP connectivity fixes
|
||||
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
|
||||
* Make selectDesktopCapturerSource param optional
|
||||
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
|
||||
* Expose APIs needed for reworked cross-signing login flow
|
||||
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
|
||||
|
||||
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0-rc.1) (2021-03-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0...v9.9.0-rc.1)
|
||||
|
||||
* Remove detailed Olm session logging
|
||||
[\#1638](https://github.com/matrix-org/matrix-js-sdk/pull/1638)
|
||||
* Add space summary suggested only param
|
||||
[\#1637](https://github.com/matrix-org/matrix-js-sdk/pull/1637)
|
||||
* Check TURN servers periodically, and at start of calls
|
||||
[\#1634](https://github.com/matrix-org/matrix-js-sdk/pull/1634)
|
||||
* Support sending invite reasons
|
||||
[\#1624](https://github.com/matrix-org/matrix-js-sdk/pull/1624)
|
||||
* Bump elliptic from 6.5.3 to 6.5.4
|
||||
[\#1636](https://github.com/matrix-org/matrix-js-sdk/pull/1636)
|
||||
* Add a function to get a room's MXC URI
|
||||
[\#1635](https://github.com/matrix-org/matrix-js-sdk/pull/1635)
|
||||
* Stop streams if the call has ended
|
||||
[\#1633](https://github.com/matrix-org/matrix-js-sdk/pull/1633)
|
||||
* Remove export keyword from global.d.ts
|
||||
[\#1631](https://github.com/matrix-org/matrix-js-sdk/pull/1631)
|
||||
* Fix IndexedDB store creation example
|
||||
[\#1445](https://github.com/matrix-org/matrix-js-sdk/pull/1445)
|
||||
* An attempt to cleanup how constraints are handled in calls
|
||||
[\#1613](https://github.com/matrix-org/matrix-js-sdk/pull/1613)
|
||||
* Extract display name patterns to constants
|
||||
[\#1628](https://github.com/matrix-org/matrix-js-sdk/pull/1628)
|
||||
* Bump pug-code-gen from 2.0.2 to 2.0.3
|
||||
[\#1630](https://github.com/matrix-org/matrix-js-sdk/pull/1630)
|
||||
* Avoid deadlocks when ensuring Olm sessions for devices
|
||||
[\#1627](https://github.com/matrix-org/matrix-js-sdk/pull/1627)
|
||||
* Filter out edits from other senders in history
|
||||
[\#1626](https://github.com/matrix-org/matrix-js-sdk/pull/1626)
|
||||
* Fix ContentHelpers export
|
||||
[\#1618](https://github.com/matrix-org/matrix-js-sdk/pull/1618)
|
||||
* Add logging to in progress Olm sessions
|
||||
[\#1621](https://github.com/matrix-org/matrix-js-sdk/pull/1621)
|
||||
* Don't ignore ICE candidates received before offer/answer
|
||||
[\#1623](https://github.com/matrix-org/matrix-js-sdk/pull/1623)
|
||||
* Better handling of send failures on VoIP events
|
||||
[\#1622](https://github.com/matrix-org/matrix-js-sdk/pull/1622)
|
||||
* Log when turn creds expire
|
||||
[\#1620](https://github.com/matrix-org/matrix-js-sdk/pull/1620)
|
||||
* Initial Spaces [MSC1772] support
|
||||
[\#1563](https://github.com/matrix-org/matrix-js-sdk/pull/1563)
|
||||
* Add logging to crypto store transactions
|
||||
[\#1617](https://github.com/matrix-org/matrix-js-sdk/pull/1617)
|
||||
* Room helper for getting type and checking if it is a space room
|
||||
[\#1610](https://github.com/matrix-org/matrix-js-sdk/pull/1610)
|
||||
|
||||
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)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.8.0-rc.1",
|
||||
"version": "9.10.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"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",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"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",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
|
||||
@@ -51,6 +51,36 @@ const signedDeviceList = {
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2 = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
|
||||
// new session and will have called claimOneTimeKeys
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,4 +282,30 @@ describe("utils", function() {
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
|
||||
function fn1() {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
await utils.sleep(1);
|
||||
expect(promiseCount).toEqual(0);
|
||||
++promiseCount;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function fn2() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
expect(promiseCount).toEqual(1);
|
||||
++promiseCount;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
await utils.chunkPromises([fn1, fn2], 1);
|
||||
expect(promiseCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,7 @@ class MockRTCPeerConnection {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
@@ -122,6 +123,7 @@ describe('Call', function() {
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!foo:bar',
|
||||
@@ -138,7 +140,9 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
await call.placeVoiceCall();
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@@ -190,4 +194,64 @@ describe('Call', function() {
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
|
||||
it('should add candidates received before answer if party ID is correct', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
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
|
||||
|
||||
// Spaces MSC1772
|
||||
SpaceChild = "org.matrix.msc1772.space.child",
|
||||
SpaceParent = "org.matrix.msc1772.space.parent",
|
||||
|
||||
// Room timeline events
|
||||
RoomRedaction = "m.room.redaction",
|
||||
RoomMessage = "m.room.message",
|
||||
@@ -87,3 +91,9 @@ export enum MsgType {
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
|
||||
|
||||
export enum RoomType {
|
||||
Space = "org.matrix.msc1772.space", // Spaces MSC1772
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -41,7 +41,7 @@ declare global {
|
||||
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
export interface DesktopCapturerConstraints {
|
||||
interface DesktopCapturerConstraints {
|
||||
audio: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
@@ -56,7 +56,7 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
|
||||
+63
-1
@@ -246,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
|
||||
|
||||
/**
|
||||
* Register a guest account.
|
||||
* This method returns the auth info needed to create a new authenticated client,
|
||||
* Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
|
||||
* ```javascript
|
||||
* const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
|
||||
* const { user_id, device_id, access_token } = tmpClient.registerGuest();
|
||||
* const client = createClient({
|
||||
* baseUrl: MATRIX_INSTANCE,
|
||||
* accessToken: access_token,
|
||||
* userId: user_id,
|
||||
* deviceId: device_id,
|
||||
* })
|
||||
* client.setGuest(true);
|
||||
* ```
|
||||
*
|
||||
* @param {Object=} opts Registration options
|
||||
* @param {Object} opts.body JSON HTTP body to provide.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: JSON object that contains:
|
||||
* { user_id, device_id, access_token, home_server }
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
||||
@@ -1518,6 +1533,21 @@ MatrixBaseApis.prototype.getDevices = function() {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets specific device details for the logged-in user
|
||||
* @param {string} device_id device to query
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getDevice = function(device_id) {
|
||||
const path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
undefined, 'GET', path, undefined, undefined,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given device
|
||||
*
|
||||
@@ -2374,3 +2404,35 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, 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?} suggestedOnly Whether to only return rooms with suggested=true.
|
||||
* @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,
|
||||
suggestedOnly,
|
||||
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,
|
||||
suggested_only: suggestedOnly,
|
||||
auto_join_only: autoJoinOnly,
|
||||
limit,
|
||||
batch,
|
||||
}, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
});
|
||||
};
|
||||
|
||||
+92
-52
@@ -61,6 +61,7 @@ import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||
|
||||
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
|
||||
const keys = [];
|
||||
@@ -393,6 +394,10 @@ export function MatrixClient(opts) {
|
||||
this._clientWellKnown = undefined;
|
||||
this._clientWellKnownPromise = undefined;
|
||||
|
||||
this._turnServers = [];
|
||||
this._turnServersExpiry = 0;
|
||||
this._checkTurnServersIntervalID = null;
|
||||
|
||||
// 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.
|
||||
// We do this so that push rules are correctly executed on events in their decrypted
|
||||
@@ -495,19 +500,8 @@ MatrixClient.prototype.rehydrateDevice = async function() {
|
||||
return;
|
||||
}
|
||||
|
||||
let getDeviceResult;
|
||||
try {
|
||||
getDeviceResult = await this._http.authedRequest(
|
||||
undefined,
|
||||
"GET",
|
||||
"/dehydrated_device",
|
||||
undefined, undefined,
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.info("could not get dehydrated device", e.toString());
|
||||
const getDeviceResult = this.getDehydratedDevice();
|
||||
if (!getDeviceResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,6 +563,27 @@ MatrixClient.prototype.rehydrateDevice = async function() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current dehydrated device, if any
|
||||
* @return {Promise} A promise of an object containing the dehydrated device
|
||||
*/
|
||||
MatrixClient.prototype.getDehydratedDevice = async function() {
|
||||
try {
|
||||
return await this._http.authedRequest(
|
||||
undefined,
|
||||
"GET",
|
||||
"/dehydrated_device",
|
||||
undefined, undefined,
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.info("could not get dehydrated device", e.toString());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the dehydration key. This will also periodically dehydrate devices to
|
||||
* the server.
|
||||
@@ -3447,11 +3462,12 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
|
||||
* @param {string} roomId
|
||||
* @param {string} userId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string} reason Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.invite = function(roomId, userId, callback) {
|
||||
return _membershipChange(this, roomId, userId, "invite", undefined,
|
||||
MatrixClient.prototype.invite = function(roomId, userId, callback, reason) {
|
||||
return _membershipChange(this, roomId, userId, "invite", reason,
|
||||
callback);
|
||||
};
|
||||
|
||||
@@ -4942,6 +4958,57 @@ MatrixClient.prototype.getTurnServers = function() {
|
||||
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;
|
||||
};
|
||||
|
||||
MatrixClient.prototype._checkTurnServers = async function() {
|
||||
if (!this._supportsVoip) {
|
||||
return;
|
||||
}
|
||||
|
||||
let credentialsGood = false;
|
||||
const remainingTime = this._turnServersExpiry - Date.now();
|
||||
if (remainingTime > TURN_CHECK_INTERVAL) {
|
||||
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
|
||||
credentialsGood = true;
|
||||
} else {
|
||||
logger.debug("Fetching new TURN credentials");
|
||||
try {
|
||||
const res = await this.turnServer();
|
||||
if (res.uris) {
|
||||
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
|
||||
// map the response to a format that can be fed to RTCPeerConnection
|
||||
const servers = {
|
||||
urls: res.uris,
|
||||
username: res.username,
|
||||
credential: res.password,
|
||||
};
|
||||
this._turnServers = [servers];
|
||||
// The TTL is in seconds but we work in ms
|
||||
this._turnServersExpiry = Date.now() + (res.ttl * 1000);
|
||||
credentialsGood = true;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to get TURN URIs", err);
|
||||
// If we get a 403, there's no point in looping forever.
|
||||
if (err.httpStatus === 403) {
|
||||
logger.info("TURN access unavailable for this account: stopping credentials checks");
|
||||
if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID);
|
||||
this._checkTurnServersIntervalID = null;
|
||||
}
|
||||
}
|
||||
// otherwise, if we failed for whatever reason, try again the next time we're called.
|
||||
}
|
||||
|
||||
return credentialsGood;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -5094,7 +5161,12 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
}
|
||||
|
||||
// periodically poll for turn servers if we support voip
|
||||
checkTurnServers(this);
|
||||
if (this._supportsVoip) {
|
||||
this._checkTurnServersIntervalID = setInterval(() => {
|
||||
this._checkTurnServers();
|
||||
}, TURN_CHECK_INTERVAL);
|
||||
this._checkTurnServers();
|
||||
}
|
||||
|
||||
if (this._syncApi) {
|
||||
// This shouldn't happen since we thought the client was not running
|
||||
@@ -5206,7 +5278,7 @@ MatrixClient.prototype.stopClient = function() {
|
||||
this._callEventHandler = null;
|
||||
}
|
||||
|
||||
global.clearTimeout(this._checkTurnServersTimeoutID);
|
||||
global.clearInterval(this._checkTurnServersIntervalID);
|
||||
if (this._clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this._clientWellKnownIntervalID);
|
||||
}
|
||||
@@ -5413,6 +5485,9 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
|
||||
}));
|
||||
events = events.filter(e => e.getType() === eventType);
|
||||
}
|
||||
if (originalEvent && relationType === "m.replace") {
|
||||
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||
}
|
||||
return {
|
||||
originalEvent,
|
||||
events,
|
||||
@@ -5420,41 +5495,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
function checkTurnServers(client) {
|
||||
if (!client._supportsVoip) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.turnServer().then(function(res) {
|
||||
if (res.uris) {
|
||||
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
|
||||
res.ttl + " secs");
|
||||
// map the response to a format that can be fed to
|
||||
// RTCPeerConnection
|
||||
const servers = {
|
||||
urls: res.uris,
|
||||
username: res.username,
|
||||
credential: res.password,
|
||||
};
|
||||
client._turnServers = [servers];
|
||||
// re-fetch when we're about to reach the TTL
|
||||
client._checkTurnServersTimeoutID = setTimeout(() => {
|
||||
checkTurnServers(client);
|
||||
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
|
||||
}
|
||||
}, function(err) {
|
||||
logger.error("Failed to get TURN URIs");
|
||||
// If we get a 403, there's no point in looping forever.
|
||||
if (err.httpStatus === 403) {
|
||||
logger.info("TURN access unavailable for this account");
|
||||
return;
|
||||
}
|
||||
client._checkTurnServersTimeoutID = setTimeout(function() {
|
||||
checkTurnServers(client);
|
||||
}, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
function _reject(callback, reject, err) {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {defer, sleep} from '../utils';
|
||||
import {chunkPromises, defer, sleep} from '../utils';
|
||||
|
||||
|
||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
|
||||
super();
|
||||
|
||||
this._cryptoStore = cryptoStore;
|
||||
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
|
||||
// userId -> promise
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
// Maximum number of user IDs per request to prevent server overload (#1619)
|
||||
this._keyDownloadChunkSize = keyDownloadChunkSize;
|
||||
|
||||
// Set whenever changes are made other than setting the sync token
|
||||
this._dirty = false;
|
||||
|
||||
@@ -780,13 +783,17 @@ class DeviceListUpdateSerialiser {
|
||||
opts.token = this._syncToken;
|
||||
}
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then(async (res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const masterKeys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
const factories = [];
|
||||
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
|
||||
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
|
||||
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
|
||||
}
|
||||
|
||||
chunkPromises(factories, 3).then(async (responses) => {
|
||||
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
|
||||
+24
-5
@@ -545,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createOutboundSession]"),
|
||||
);
|
||||
return newSessionId;
|
||||
};
|
||||
@@ -605,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createInboundSession]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -619,8 +621,10 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
* @return {Promise<string[]>} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
const log = logger.withPrefix("[getSessionIdsForDevice]");
|
||||
|
||||
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 {
|
||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -638,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
},
|
||||
);
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
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.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* 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
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
);
|
||||
|
||||
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.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @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) {
|
||||
logger.log("waiting for olm session to be created");
|
||||
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this._sessionsInProgress[deviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -727,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
|
||||
}
|
||||
});
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return info;
|
||||
@@ -761,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[encryptMessage]"),
|
||||
);
|
||||
return res;
|
||||
};
|
||||
@@ -794,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[decryptMessage]"),
|
||||
);
|
||||
return payloadString;
|
||||
};
|
||||
@@ -825,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[matchesSession]"),
|
||||
);
|
||||
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) {
|
||||
@@ -1310,6 +1327,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1369,6 +1387,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[getInboundGroupSessionKey]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
import {getPrefixedLogger, logger} from '../../logger';
|
||||
import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
@@ -736,7 +736,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(
|
||||
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
||||
getPrefixedLogger(`[${this._roomId}]`),
|
||||
logger.withPrefix(`[${this._roomId}]`),
|
||||
);
|
||||
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
||||
|
||||
|
||||
+43
-44
@@ -183,7 +183,7 @@ export async function getExistingOlmSessions(
|
||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||
* 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
|
||||
* an Object mapping from userId to deviceId to
|
||||
@@ -208,6 +208,35 @@ export async function ensureOlmSessionsForDevices(
|
||||
const result = {};
|
||||
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 [, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
result[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
@@ -232,41 +261,21 @@ export async function ensureOlmSessionsForDevices(
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
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 forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key],
|
||||
key, resolveSession[key], log,
|
||||
);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so unmark it and unblock anything that was
|
||||
// waiting
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolveSession[key].resolve();
|
||||
delete resolveSession[key];
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
// unblock anything that was waiting
|
||||
resolveSession[key]();
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
log.info(`Forcing new Olm session for ${userId}:${deviceId}`);
|
||||
log.info(`Forcing new Olm session ${forWhom}`);
|
||||
} else {
|
||||
log.info(`Making new Olm session for ${userId}:${deviceId}`);
|
||||
log.info(`Making new Olm session ${forWhom}`);
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
@@ -284,14 +293,6 @@ export async function ensureOlmSessionsForDevices(
|
||||
const oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
let res;
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
// If your homeserver takes a nap here and never replies, this process
|
||||
// would hang indefinitely. While that's easily fixed by setting a
|
||||
// timeout on this request, let's first log whether that's the root
|
||||
// cause we're seeing in practice.
|
||||
// See also https://github.com/vector-im/element-web/issues/16194
|
||||
const otkTimeoutLogger = setTimeout(() => {
|
||||
log.error(`Homeserver never replied while claiming ${taskDetail}`);
|
||||
}, otkTimeout);
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
@@ -300,22 +301,20 @@ export async function ensureOlmSessionsForDevices(
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
resolver.resolve();
|
||||
resolver();
|
||||
}
|
||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(otkTimeoutLogger);
|
||||
}
|
||||
|
||||
if (failedServers && "failures" in res) {
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otk_res = res.one_time_keys || {};
|
||||
const otkResult = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
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++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
@@ -347,7 +346,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
`for device ${userId}:${deviceId}`,
|
||||
);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -357,12 +356,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo,
|
||||
).then((sid) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve(sid);
|
||||
resolveSession[key](sid);
|
||||
}
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}, (e) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
|
||||
export const VERSION = 9;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -34,6 +35,7 @@ export class Backend {
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this._nextTxnId = 0;
|
||||
|
||||
// make sure we close the db on `onversionchange` - otherwise
|
||||
// attempts to delete the database will block (and subsequent
|
||||
@@ -757,10 +759,27 @@ export class Backend {
|
||||
}));
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
doTxn(mode, stores, func, log = logger) {
|
||||
let startTime;
|
||||
let description;
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
const txnId = this._nextTxnId++;
|
||||
startTime = Date.now();
|
||||
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
|
||||
log.debug(`Starting ${description}`);
|
||||
}
|
||||
const txn = this._db.transaction(stores, mode);
|
||||
const promise = promiseifyTxn(txn);
|
||||
const result = func(txn);
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
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 result;
|
||||
});
|
||||
|
||||
@@ -596,6 +596,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(*)} func Function called with the
|
||||
* transaction object: an opaque object that should be passed
|
||||
* to store functions.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Promise} Promise that resolves with the result of the `func`
|
||||
* when the transaction is complete. If the backend is
|
||||
* async (ie. the indexeddb backend) any of the callback
|
||||
@@ -603,8 +604,8 @@ export class IndexedDBCryptoStore {
|
||||
* reject with that exception. On synchronous backends, the
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
doTxn(mode, stores, func) {
|
||||
return this._backend.doTxn(mode, stores, func);
|
||||
doTxn(mode, stores, func, log) {
|
||||
return this._backend.doTxn(mode, stores, func, log);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,9 @@ export class ToDeviceChannel {
|
||||
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||
// the request has picked a ready or start event, tell the other devices about it
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
|
||||
const nonChosenDevices = this._devices.filter(
|
||||
d => d !== this._deviceId && d !== this._client.getDeviceId(),
|
||||
);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
code: "m.accepted",
|
||||
|
||||
+1
-1
@@ -271,10 +271,10 @@ MatrixHttpApi.prototype = {
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
let resp;
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var resp;
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
throw new AbortError();
|
||||
|
||||
+14
-3
@@ -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}.
|
||||
* 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);
|
||||
|
||||
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}`);
|
||||
if (prefixLogger.prefix !== prefix) {
|
||||
// Only do this setup work the first time through, as loggers are saved by name.
|
||||
extendLogger(prefixLogger);
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.setLevel(log.levels.DEBUG);
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export * from "./store/session/webstorage";
|
||||
export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export * from "./content-repo";
|
||||
export const ContentHelpers = import("./content-helpers");
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
setAudioOutput as setMatrixCallAudioOutput,
|
||||
|
||||
@@ -290,6 +290,9 @@ RoomMember.prototype.getMxcAvatarUrl = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
const MXID_PATTERN = /@.+:.+/;
|
||||
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||
|
||||
function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||
if (!displayName || displayName === selfUserId) {
|
||||
return selfUserId;
|
||||
@@ -308,13 +311,13 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||
// Next check if the name contains something that look like a mxid
|
||||
// If it does, it may be someone trying to impersonate someone else
|
||||
// Show full mxid in this case
|
||||
let disambiguate = /@.+:.+/.test(displayName);
|
||||
let disambiguate = MXID_PATTERN.test(displayName);
|
||||
|
||||
if (!disambiguate) {
|
||||
// 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
|
||||
// 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) {
|
||||
|
||||
+32
-2
@@ -30,7 +30,7 @@ import {RoomMember} from "./room-member";
|
||||
import {RoomSummary} from "./room-summary";
|
||||
import {logger} from '../logger';
|
||||
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
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -818,7 +818,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
|
||||
*/
|
||||
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
allowDefault) {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
if (allowDefault === undefined) {
|
||||
allowDefault = true;
|
||||
}
|
||||
@@ -836,6 +836,15 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the mxc avatar url for the room, if one was set.
|
||||
* @return {string} the mxc avatar url or falsy
|
||||
*/
|
||||
Room.prototype.getMxcAvatarUrl = function() {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
return roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the aliases this room has according to the room's state
|
||||
* The aliases returned by this function may not necessarily
|
||||
@@ -1856,6 +1865,27 @@ Room.prototype.getJoinRule = function() {
|
||||
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
|
||||
* room state.
|
||||
|
||||
@@ -435,7 +435,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @return {Promise} Resolves if the data was persisted.
|
||||
*/
|
||||
_persistSyncData: function(nextBatch, roomsData, groupsData) {
|
||||
logger.log("Persisting sync data up to ", nextBatch);
|
||||
logger.log("Persisting sync data up to", nextBatch);
|
||||
return utils.promiseTry(() => {
|
||||
const txn = this.db.transaction(["sync"], "readwrite");
|
||||
const store = txn.objectStore("sync");
|
||||
|
||||
@@ -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
|
||||
* the data is eagerly fetched when <code>startup()</code> is called.
|
||||
* <pre>
|
||||
* let opts = { localStorage: window.localStorage };
|
||||
* let store = new IndexedDBStore();
|
||||
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
||||
* let store = new IndexedDBStore(opts);
|
||||
* await store.startup(); // load from indexed db
|
||||
* let client = sdk.createClient({
|
||||
* store: store,
|
||||
|
||||
@@ -745,6 +745,15 @@ export function promiseTry<T>(fn: () => T): Promise<T> {
|
||||
return new Promise((resolve) => resolve(fn()));
|
||||
}
|
||||
|
||||
// Creates and awaits all promises, running no more than `chunkSize` at the same time
|
||||
export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
for (let i = 0; i < fns.length; i += chunkSize) {
|
||||
results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn()))));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// We need to be able to access the Node.js crypto library from within the
|
||||
// Matrix SDK without needing to `require("crypto")`, which will fail in
|
||||
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
||||
|
||||
+241
-143
@@ -174,6 +174,11 @@ export enum CallErrorCode {
|
||||
SignallingFailed = 'signalling_timeout',
|
||||
}
|
||||
|
||||
enum ConstraintsType {
|
||||
Audio = "audio",
|
||||
Video = "video",
|
||||
}
|
||||
|
||||
/**
|
||||
* The version field that we set in m.call.* events
|
||||
*/
|
||||
@@ -251,8 +256,6 @@ export class MatrixCall extends EventEmitter {
|
||||
private localAVStream: MediaStream;
|
||||
private inviteOrAnswerSent: 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'.
|
||||
private config: MediaStreamConstraints;
|
||||
private successor: MatrixCall;
|
||||
@@ -284,6 +287,11 @@ export class MatrixCall extends EventEmitter {
|
||||
private makingOffer: 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) {
|
||||
super();
|
||||
this.roomId = opts.roomId;
|
||||
@@ -291,9 +299,6 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = null;
|
||||
this.forceTURN = opts.forceTURN;
|
||||
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
|
||||
this.turnServers = opts.turnServers || [];
|
||||
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
||||
@@ -328,10 +333,11 @@ export class MatrixCall extends EventEmitter {
|
||||
* Place a voice call to this room.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
placeVoiceCall() {
|
||||
async placeVoiceCall() {
|
||||
logger.debug("placeVoiceCall");
|
||||
this.checkForErrorListener();
|
||||
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Voice));
|
||||
const constraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
this.type = CallType.Voice;
|
||||
}
|
||||
|
||||
@@ -343,12 +349,13 @@ export class MatrixCall extends EventEmitter {
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
|
||||
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
|
||||
logger.debug("placeVideoCall");
|
||||
this.checkForErrorListener();
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Video));
|
||||
const constraints = getUserMediaContraints(ConstraintsType.Video);
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
this.type = CallType.Video;
|
||||
}
|
||||
|
||||
@@ -365,61 +372,37 @@ export class MatrixCall extends EventEmitter {
|
||||
async placeScreenSharingCall(
|
||||
remoteVideoElement: HTMLVideoElement,
|
||||
localVideoElement: HTMLVideoElement,
|
||||
selectDesktopCapturerSource: () => Promise<DesktopCapturerSource>,
|
||||
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
|
||||
) {
|
||||
logger.debug("placeScreenSharingCall");
|
||||
this.checkForErrorListener();
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
|
||||
if (window.electron?.getDesktopCapturerSources) {
|
||||
// We have access to getDesktopCapturerSources()
|
||||
logger.debug("Electron getDesktopCapturerSources() is available...");
|
||||
try {
|
||||
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);
|
||||
try {
|
||||
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
|
||||
if (!screenshareConstraints) return;
|
||||
if (window.electron?.getDesktopCapturerSources) {
|
||||
// We are using Electron
|
||||
logger.debug("Getting screen stream using getUserMedia()...");
|
||||
this.screenSharingStream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
|
||||
} else {
|
||||
// We are not using Electron
|
||||
logger.debug("Getting screen stream using getDisplayMedia()...");
|
||||
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
|
||||
}
|
||||
|
||||
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 {
|
||||
/* 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...");
|
||||
try {
|
||||
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
logger.debug("Got screen stream, requesting audio stream...");
|
||||
const audioConstraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||
this.placeCallWithConstraints(audioConstraints);
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(
|
||||
CallErrorCode.NoUserMedia,
|
||||
"Failed to get screen-sharing stream: ", err,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.type = CallType.Video;
|
||||
}
|
||||
|
||||
@@ -541,11 +524,24 @@ export class MatrixCall extends EventEmitter {
|
||||
* @param {MatrixEvent} event The m.call.invite event
|
||||
*/
|
||||
async initWithInvite(event: MatrixEvent) {
|
||||
this.msg = event.getContent();
|
||||
const invite = event.getContent();
|
||||
this.direction = CallDirection.Inbound;
|
||||
|
||||
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
||||
// poll and keep the credentials valid so this should be instant.
|
||||
const haveTurnCreds = await this.client._checkTurnServers();
|
||||
if (!haveTurnCreds) {
|
||||
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
||||
}
|
||||
|
||||
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 {
|
||||
await this.peerConn.setRemoteDescription(this.msg.offer);
|
||||
await this.peerConn.setRemoteDescription(invite.offer);
|
||||
await this.addBufferedIceCandidates();
|
||||
} catch (e) {
|
||||
logger.debug("Failed to set remote description", e);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
@@ -564,13 +560,6 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||
|
||||
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()) {
|
||||
setTimeout(() => {
|
||||
@@ -584,7 +573,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
this.emit(CallEvent.Hangup);
|
||||
}
|
||||
}, this.msg.lifetime - event.getLocalAge());
|
||||
}, invite.lifetime - event.getLocalAge());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +585,6 @@ export class MatrixCall extends EventEmitter {
|
||||
// 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
|
||||
// come in reverse order and we want to remember that a call has been hung up)
|
||||
this.msg = event.getContent();
|
||||
this.setState(CallState.Ended);
|
||||
}
|
||||
|
||||
@@ -611,7 +599,11 @@ export class MatrixCall extends EventEmitter {
|
||||
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
|
||||
|
||||
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);
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
this.waitForLocalAVStream = true;
|
||||
@@ -668,6 +660,8 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
logger.debug("Ending call " + this.callId);
|
||||
this.terminate(CallParty.Local, reason, !suppressEvent);
|
||||
// We don't want to send hangup here if we didn't even get to sending an invite
|
||||
if (this.state === CallState.WaitLocalMedia) return;
|
||||
const content = {};
|
||||
// Continue to send no reason for user hangups temporarily, until
|
||||
// clients understand the user_hangup reason (voip v1)
|
||||
@@ -841,8 +835,11 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
if (this.callHasEnded()) {
|
||||
this.stopAllMedia();
|
||||
return;
|
||||
}
|
||||
this.localAVStream = stream;
|
||||
logger.info("Got local AV stream with id " + this.localAVStream.id);
|
||||
|
||||
this.setState(CallState.CreateOffer);
|
||||
|
||||
@@ -868,11 +865,8 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
logger.info("Got local AV stream with id " + this.localAVStream.id);
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = this.createPeerConnection();
|
||||
|
||||
for (const audioTrack of stream.getAudioTracks()) {
|
||||
logger.info("Adding audio track with id " + audioTrack.id);
|
||||
@@ -886,7 +880,7 @@ export class MatrixCall extends EventEmitter {
|
||||
// Now we wait for the negotiationneeded event
|
||||
};
|
||||
|
||||
private sendAnswer() {
|
||||
private async sendAnswer() {
|
||||
const answerContent = {
|
||||
answer: {
|
||||
sdp: this.peerConn.localDescription.sdp,
|
||||
@@ -908,12 +902,12 @@ export class MatrixCall extends EventEmitter {
|
||||
logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`);
|
||||
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,
|
||||
// we may have candidates queued up, so send them now.
|
||||
this.inviteOrAnswerSent = true;
|
||||
this.sendCandidateQueue();
|
||||
}).catch((error) => {
|
||||
} catch (error) {
|
||||
// We've failed to answer: back to the ringing state
|
||||
this.setState(CallState.Ringing);
|
||||
this.client.cancelPendingEvent(error.event);
|
||||
@@ -926,7 +920,11 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
this.emit(CallEvent.Error, new CallError(code, message, 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) => {
|
||||
@@ -990,7 +988,7 @@ export class MatrixCall extends EventEmitter {
|
||||
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (event.candidate) {
|
||||
logger.debug(
|
||||
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
|
||||
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
|
||||
event.candidate.candidate,
|
||||
);
|
||||
|
||||
@@ -1024,43 +1022,39 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
onRemoteIceCandidatesReceived(ev: MatrixEvent) {
|
||||
async onRemoteIceCandidatesReceived(ev: MatrixEvent) {
|
||||
if (this.callHasEnded()) {
|
||||
//debuglog("Ignoring remote ICE candidate because call has ended");
|
||||
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;
|
||||
if (!cands) {
|
||||
logger.info("Ignoring candidates event with no candidates!");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
|
||||
|
||||
if (this.opponentPartyId === undefined) {
|
||||
// we haven't picked an opponent yet so save the candidates
|
||||
logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
|
||||
const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
|
||||
bufferedCands.push(...cands);
|
||||
this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
|
||||
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;
|
||||
}
|
||||
|
||||
await this.addIceCandidates(cands);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1068,11 +1062,14 @@ export class MatrixCall extends EventEmitter {
|
||||
* @param {Object} msg
|
||||
*/
|
||||
async onAnswerReceived(event: MatrixEvent) {
|
||||
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`);
|
||||
|
||||
if (this.callHasEnded()) {
|
||||
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opponentPartyId !== null) {
|
||||
if (this.opponentPartyId !== undefined) {
|
||||
logger.info(
|
||||
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
|
||||
`we already have an answer/reject from ${this.opponentPartyId}`,
|
||||
@@ -1080,12 +1077,8 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.opponentVersion = event.getContent().version;
|
||||
if (this.opponentVersion !== 0) {
|
||||
this.opponentPartyId = event.getContent().party_id || null;
|
||||
}
|
||||
this.opponentCaps = event.getContent().capabilities || {};
|
||||
this.opponentMember = event.sender;
|
||||
this.chooseOpponent(event);
|
||||
await this.addBufferedIceCandidates();
|
||||
|
||||
this.setState(CallState.Connecting);
|
||||
|
||||
@@ -1260,19 +1253,9 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
try {
|
||||
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) {
|
||||
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 message = "Signalling failed";
|
||||
@@ -1287,6 +1270,22 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
this.emit(CallEvent.Error, new CallError(code, message, error));
|
||||
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 +1622,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private sendCandidateQueue() {
|
||||
private async sendCandidateQueue() {
|
||||
if (this.candidateSendQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1635,20 +1634,28 @@ export class MatrixCall extends EventEmitter {
|
||||
candidates: cands,
|
||||
};
|
||||
logger.debug("Attempting to send " + cands.length + " candidates");
|
||||
this.sendVoipEvent(EventType.CallCandidates, content).then(() => {
|
||||
this.candidateSendTries = 0;
|
||||
this.sendCandidateQueue();
|
||||
}, (error) => {
|
||||
for (let i = 0; i < cands.length; i++) {
|
||||
this.candidateSendQueue.push(cands[i]);
|
||||
}
|
||||
try {
|
||||
await this.sendVoipEvent(EventType.CallCandidates, content);
|
||||
} catch (error) {
|
||||
// don't retry this event: we'll send another one later as we might
|
||||
// have more candidates by then.
|
||||
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) {
|
||||
logger.debug(
|
||||
"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;
|
||||
}
|
||||
|
||||
@@ -1658,7 +1665,7 @@ export class MatrixCall extends EventEmitter {
|
||||
setTimeout(() => {
|
||||
this.sendCandidateQueue();
|
||||
}, delayMs);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
|
||||
@@ -1668,11 +1675,18 @@ export class MatrixCall extends EventEmitter {
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
this.direction = CallDirection.Outbound;
|
||||
this.config = constraints;
|
||||
// It would be really nice if we could start gathering candidates at this point
|
||||
// so the ICE agent could be gathering while we open our media devices: we already
|
||||
// know the type of the call and therefore what tracks we want to send.
|
||||
// Perhaps we could do this by making fake tracks now and then using replaceTrack()
|
||||
// once we have the actual tracks? (Can we make fake tracks?)
|
||||
|
||||
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
||||
// poll and keep the credentials valid so this should be instant.
|
||||
const haveTurnCreds = await this.client._checkTurnServers();
|
||||
if (!haveTurnCreds) {
|
||||
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
||||
}
|
||||
|
||||
// create the peer connection now so it can be gathering candidates while we get user
|
||||
// media (assuming a candidate pool size is configured)
|
||||
this.peerConn = this.createPeerConnection();
|
||||
|
||||
try {
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
this.gotUserMediaForInvite(mediaStream);
|
||||
@@ -1702,9 +1716,64 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
private partyIdMatches(msg): boolean {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async addBufferedIceCandidates() {
|
||||
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
|
||||
if (bufferedCands) {
|
||||
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
|
||||
await this.addIceCandidates(bufferedCands);
|
||||
}
|
||||
this.remoteCandidateBuffer = null;
|
||||
}
|
||||
|
||||
private async 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");
|
||||
continue;
|
||||
}
|
||||
logger.debug("Call " + this.callId + " got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
||||
try {
|
||||
await this.peerConn.addIceCandidate(cand);
|
||||
} catch (err) {
|
||||
if (!this.ignoreOffer) {
|
||||
logger.info("Failed to add remote ICE candidate", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
|
||||
@@ -1713,17 +1782,19 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function getUserMediaVideoContraints(callType: CallType) {
|
||||
function getUserMediaContraints(type: ConstraintsType) {
|
||||
const isWebkit = !!navigator.webkitGetUserMedia;
|
||||
|
||||
switch (callType) {
|
||||
case CallType.Voice:
|
||||
switch (type) {
|
||||
case ConstraintsType.Audio: {
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||
}, video: false,
|
||||
},
|
||||
video: false,
|
||||
};
|
||||
case CallType.Video:
|
||||
}
|
||||
case ConstraintsType.Video: {
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||
@@ -1738,6 +1809,33 @@ function getUserMediaVideoContraints(callType: CallType) {
|
||||
height: isWebkit ? { exact: 360 } : { ideal: 360 },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ export class CallEventHandler {
|
||||
);
|
||||
}
|
||||
|
||||
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
|
||||
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
||||
forceTURN: this.client._forceTURN,
|
||||
});
|
||||
|
||||
@@ -1876,10 +1876,10 @@ bluebird@^3.5.0, bluebird@^3.7.2:
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
||||
version "4.11.9"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
||||
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
bn.js@^5.0.0, bn.js@^5.1.1:
|
||||
version "5.1.3"
|
||||
@@ -1922,7 +1922,7 @@ braces@^3.0.1, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
brorand@^1.0.1:
|
||||
brorand@^1.0.1, brorand@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||
@@ -2796,17 +2796,17 @@ electron-to-chromium@^1.3.634:
|
||||
integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ==
|
||||
|
||||
elliptic@^6.5.3:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
|
||||
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||
dependencies:
|
||||
bn.js "^4.4.0"
|
||||
brorand "^1.0.1"
|
||||
bn.js "^4.11.9"
|
||||
brorand "^1.1.0"
|
||||
hash.js "^1.0.0"
|
||||
hmac-drbg "^1.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.0"
|
||||
hmac-drbg "^1.0.1"
|
||||
inherits "^2.0.4"
|
||||
minimalistic-assert "^1.0.1"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
emittery@^0.7.1:
|
||||
version "0.7.2"
|
||||
@@ -3820,7 +3820,7 @@ he@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
||||
@@ -5069,11 +5069,16 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
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:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||
@@ -5255,7 +5260,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
|
||||
minimalistic-crypto-utils@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
@@ -5917,9 +5922,9 @@ pug-attrs@^2.0.4:
|
||||
pug-runtime "^2.0.5"
|
||||
|
||||
pug-code-gen@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2"
|
||||
integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.3.tgz#122eb9ada9b5bf601705fe15aaa0a7d26bc134ab"
|
||||
integrity sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==
|
||||
dependencies:
|
||||
constantinople "^3.1.2"
|
||||
doctypes "^1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user