Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae6e2cca27 | |||
| bd920eef1f | |||
| ed6d4e5f6c | |||
| accfa325b5 | |||
| b6ef8d95cd | |||
| c1144e3810 | |||
| 8663fd402b | |||
| f34052fd31 | |||
| 27d75a269f | |||
| d208a7fc5f | |||
| 702e16e3df | |||
| 12050b14f0 | |||
| 2d4a4f1736 | |||
| cd38fb9b4c | |||
| 7941b16ec4 | |||
| 56ea4b8741 | |||
| 6dabfcda6f | |||
| 0b7b35f800 | |||
| 9fb2fbaeec | |||
| 977682d37f | |||
| 0bafe263d7 | |||
| 1a8fced80e | |||
| 1c4d0b5e99 | |||
| 844a2b457c | |||
| ccf06f2216 |
@@ -1,3 +1,20 @@
|
||||
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)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.9.0",
|
||||
"version": "9.10.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+31
-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
|
||||
*
|
||||
|
||||
+23
-13
@@ -500,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;
|
||||
}
|
||||
|
||||
@@ -574,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
+21
-10
@@ -372,7 +372,7 @@ export class MatrixCall extends EventEmitter {
|
||||
async placeScreenSharingCall(
|
||||
remoteVideoElement: HTMLVideoElement,
|
||||
localVideoElement: HTMLVideoElement,
|
||||
selectDesktopCapturerSource: () => Promise<DesktopCapturerSource>,
|
||||
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
|
||||
) {
|
||||
logger.debug("placeScreenSharingCall");
|
||||
this.checkForErrorListener();
|
||||
@@ -541,6 +541,7 @@ export class MatrixCall extends EventEmitter {
|
||||
this.chooseOpponent(event);
|
||||
try {
|
||||
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);
|
||||
@@ -659,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)
|
||||
@@ -985,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,
|
||||
);
|
||||
|
||||
@@ -1019,7 +1022,7 @@ 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;
|
||||
@@ -1051,7 +1054,7 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addIceCandidates(cands);
|
||||
await this.addIceCandidates(cands);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1059,7 +1062,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1072,6 +1078,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
this.chooseOpponent(event);
|
||||
await this.addBufferedIceCandidates();
|
||||
|
||||
this.setState(CallState.Connecting);
|
||||
|
||||
@@ -1722,6 +1729,8 @@ export class MatrixCall extends EventEmitter {
|
||||
// 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
|
||||
@@ -1735,30 +1744,32 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
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}`);
|
||||
this.addIceCandidates(bufferedCands);
|
||||
await this.addIceCandidates(bufferedCands);
|
||||
}
|
||||
this.remoteCandidateBuffer = null;
|
||||
}
|
||||
|
||||
private addIceCandidates(cands: RTCIceCandidate[]) {
|
||||
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");
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
||||
logger.debug("Call " + this.callId + " got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
||||
try {
|
||||
this.peerConn.addIceCandidate(cand);
|
||||
await this.peerConn.addIceCandidate(cand);
|
||||
} catch (err) {
|
||||
if (!this.ignoreOffer) {
|
||||
logger.info("Failed to add remore ICE candidate", err);
|
||||
logger.info("Failed to add remote ICE candidate", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user