Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c81c295c7 | |||
| 56dfa0c755 | |||
| 8c37d9ac9a | |||
| e40b8461f7 | |||
| a3f45b466a | |||
| 672ad68c64 | |||
| 4ccec13739 | |||
| 92cfbf655f | |||
| fbef701179 | |||
| 0415b9cf4c | |||
| cb9a9e8d50 | |||
| 6021c1c6b1 | |||
| 655be2fa2e | |||
| 98491a63a7 | |||
| acd7f15c83 | |||
| 5020d4e99f | |||
| 9693c30209 | |||
| 2b6f8adc64 | |||
| 822f5927e5 | |||
| 0f6e9d7b9d | |||
| 99f3e3f09e | |||
| aa81c96a98 | |||
| 9d532b6c72 | |||
| 4c63906b8f | |||
| dd2a870227 | |||
| 88948c3cfd | |||
| b33dcfe6ff | |||
| 2c15bdae04 | |||
| 2f45633312 | |||
| fdd42fbc6d | |||
| 54a6f5d425 | |||
| 68d9662fe5 | |||
| 4f0987da01 | |||
| dd069647d1 | |||
| 4523ae7d29 | |||
| 19e5eda773 |
@@ -1,3 +1,48 @@
|
||||
Changes in [0.7.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8) (2017-05-22)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8-rc.1...v0.7.8)
|
||||
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [0.7.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8-rc.1) (2017-05-19)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7...v0.7.8-rc.1)
|
||||
|
||||
* Attempt to rework the release-tarball-signing stuff
|
||||
[\#438](https://github.com/matrix-org/matrix-js-sdk/pull/438)
|
||||
* ability to specify webrtc audio/video inputs for the lib to request
|
||||
[\#427](https://github.com/matrix-org/matrix-js-sdk/pull/427)
|
||||
* make screen sharing call FF friendly :D
|
||||
[\#434](https://github.com/matrix-org/matrix-js-sdk/pull/434)
|
||||
* Fix race in device list updates
|
||||
[\#431](https://github.com/matrix-org/matrix-js-sdk/pull/431)
|
||||
* WebRTC: Support recvonly for video for those without a webcam
|
||||
[\#424](https://github.com/matrix-org/matrix-js-sdk/pull/424)
|
||||
* Update istanbul to remove minimatch DoS Warning
|
||||
[\#422](https://github.com/matrix-org/matrix-js-sdk/pull/422)
|
||||
* webrtc/call: Make it much less likely that callIds collide locally
|
||||
[\#423](https://github.com/matrix-org/matrix-js-sdk/pull/423)
|
||||
* Automatically complete dummy auth
|
||||
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
|
||||
* Don't leave the gh-pages branch checked out
|
||||
[\#418](https://github.com/matrix-org/matrix-js-sdk/pull/418)
|
||||
|
||||
Changes in [0.7.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7) (2017-04-25)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7-rc.1...v0.7.7)
|
||||
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [0.7.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7-rc.1) (2017-04-21)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6...v0.7.7-rc.1)
|
||||
|
||||
* Automatically complete dummy auth
|
||||
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
|
||||
|
||||
|
||||
Changes in [0.7.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6) (2017-04-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.2...v0.7.6)
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -62,7 +62,7 @@
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.3.13",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.4.0",
|
||||
"lolex": "^1.5.2",
|
||||
"mocha": "^3.2.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglifyjs": "^2.4.10",
|
||||
"uglify-js": "^2.8.26",
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
+48
-13
@@ -178,27 +178,61 @@ if [ $dodist -eq 0 ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
|
||||
# also make a signature for the source tarball.
|
||||
project_name=`jq -r '.name' package.json`
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
git archive --format tgz --prefix="${project_name}-${release}/" "$tag" |
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig -
|
||||
assets="$assets -a $source_sigfile"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag
|
||||
git push origin "$tag"
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github.com:#https://github.com/#' -e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
@@ -237,6 +271,7 @@ fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ TestClient.prototype.toString = function() {
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
console.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
@@ -403,29 +403,6 @@ describe("MatrixClient crypto", function() {
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it('Ali knows the difference between a new user and one with no devices',
|
||||
function(done) {
|
||||
aliTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:id': {},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = aliTestClient.client.downloadKeys(['@bob:id']);
|
||||
const p2 = aliTestClient.httpBackend.flush('/keys/query', 1);
|
||||
|
||||
q.all([p1, p2]).then(function() {
|
||||
const devices = aliTestClient.storage.getEndToEndDevicesForUser(
|
||||
'@bob:id',
|
||||
);
|
||||
expect(utils.keys(devices).length).toEqual(0);
|
||||
|
||||
// request again: should be no more requests
|
||||
return aliTestClient.client.downloadKeys(['@bob:id']);
|
||||
}).nodeify(done);
|
||||
},
|
||||
);
|
||||
|
||||
it("Bob uploads device keys", function() {
|
||||
return q()
|
||||
.then(bobUploadsDeviceKeys);
|
||||
@@ -673,6 +650,27 @@ describe("MatrixClient crypto", function() {
|
||||
return q()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
|
||||
// ali will only care about bob's new_device if she is tracking
|
||||
// bob's devices, which she will do if we enable encryption
|
||||
.then(aliEnablesEncryption)
|
||||
|
||||
.then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[aliUserId]: {},
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
return aliTestClient.httpBackend.flush('/keys/query', 1);
|
||||
})
|
||||
|
||||
// make sure that the initial key download has completed
|
||||
// (downloadKeys will wait until it does)
|
||||
.then(() => {
|
||||
return aliTestClient.client.downloadKeys([bobUserId]);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
|
||||
+29
-48
@@ -1001,7 +1001,19 @@ describe("megolm", function() {
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
@@ -1020,7 +1032,15 @@ describe("megolm", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(2);
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
@@ -1030,56 +1050,17 @@ describe("megolm", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("Device list downloads before /changes shouldn't affect sync token",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126#issuecomment-279374939
|
||||
aliceTestClient.storage.storeEndToEndDeviceSyncToken(0);
|
||||
aliceTestClient.storage.storeEndToEndRoom(ROOM_ID, {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
});
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([aliceTestClient.userId, '@bob:xyz']));
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(() => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {device_keys: {'@bob:xyz': {}}},
|
||||
);
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(0);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'GET', '/keys/changes',
|
||||
).check((req) => {
|
||||
expect(req.queryParams.from).toEqual(0);
|
||||
expect(req.queryParams.to).toEqual(1);
|
||||
}).respond(200, {changed: ['@bob:xyz']});
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/keys/changes');
|
||||
}).then((flushed) => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {device_keys: {'@bob:xyz': {}}},
|
||||
);
|
||||
return aliceTestClient.httpBackend.flush('/keys/query');
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// let the client finish processing the keys
|
||||
return q.delay(10);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function(done) {
|
||||
let messageEncrypted;
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import testUtils from '../../test-utils';
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
import expect from 'expect';
|
||||
import q from 'q';
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test1:sw1v.org": {
|
||||
"HGKAWHRVJQ": {
|
||||
"signatures": {
|
||||
"@test1:sw1v.org": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
|
||||
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
|
||||
},
|
||||
},
|
||||
"user_id": "@test1:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ":
|
||||
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let sessionStore;
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
};
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
return new DeviceList(baseApis, sessionStore, mockOlm);
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an outdated devicelist on an invalidation while an " +
|
||||
"update is in progress", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
downloadSpy.reset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
});
|
||||
|
||||
return prom1.then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
console.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.reset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
}).then(() => {
|
||||
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,20 @@ MatrixBaseApis.prototype.makeTxnId = function() {
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
|
||||
/**
|
||||
* Check whether a username is available prior to registration. An error response
|
||||
* indicates an invalid/unavailable username.
|
||||
* @param {string} username The username to check the availability of.
|
||||
* @return {module:client.Promise} Resolves: to `true`.
|
||||
*/
|
||||
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", '/register/available', { username: username },
|
||||
).then((response) => {
|
||||
return response.available;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
@@ -470,6 +484,34 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a marker to indicate the point in a room before which the user has read every
|
||||
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
|
||||
* and displayed as a horizontal line in the timeline that is visually distinct to the
|
||||
* position of the user's own read receipt.
|
||||
* @param {string} roomId ID of the room that has been read
|
||||
* @param {string} rmEventId ID of the event that has been read
|
||||
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
|
||||
* for convenience because the RR and the RM are commonly updated at the same time as
|
||||
* each other. Optional.
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
|
||||
function(roomId, rmEventId, rrEventId) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
const content = {
|
||||
"m.fully_read": rmEventId,
|
||||
"m.read": rrEventId,
|
||||
};
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "POST", path, undefined, content,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room Directory operations
|
||||
// =========================
|
||||
|
||||
@@ -1201,6 +1201,33 @@ MatrixClient.prototype.sendReadReceipt = function(event, callback) {
|
||||
return this.sendReceipt(event, "m.read", callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a marker to indicate the point in a room before which the user has read every
|
||||
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
|
||||
* and displayed as a horizontal line in the timeline that is visually distinct to the
|
||||
* position of the user's own read receipt.
|
||||
* @param {string} roomId ID of the room that has been read
|
||||
* @param {string} eventId ID of the event that has been read
|
||||
* @param {string} rrEvent the event tracked by the read receipt. This is here for
|
||||
* convenience because the RR and the RM are commonly updated at the same time as each
|
||||
* other. The local echo of this receipt will be done if set. Optional.
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) {
|
||||
const rmEventId = eventId;
|
||||
let rrEventId;
|
||||
|
||||
// Add the optional RR update, do local echo like `sendReceipt`
|
||||
if (rrEvent) {
|
||||
rrEventId = rrEvent.getId();
|
||||
const room = this.getRoom(roomId);
|
||||
if (room) {
|
||||
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
|
||||
}
|
||||
}
|
||||
|
||||
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a preview of the given URL as of (roughly) the given point in time,
|
||||
|
||||
+251
-150
@@ -26,35 +26,35 @@ import q from 'q';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import olmlib from './olmlib';
|
||||
|
||||
// constants for DeviceList._deviceTrackingStatus
|
||||
// const TRACKING_STATUS_NOT_TRACKED = 0;
|
||||
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
|
||||
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
|
||||
const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList {
|
||||
constructor(baseApis, sessionStore, olmDevice) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._olmDevice = olmDevice;
|
||||
this._serialiser = new DeviceListUpdateSerialiser(
|
||||
baseApis, sessionStore, olmDevice,
|
||||
);
|
||||
|
||||
// users with outdated device lists
|
||||
// userId -> true
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
// which users we are tracking device status for.
|
||||
// userId -> TRACKING_STATUS_*
|
||||
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
|
||||
for (const u of Object.keys(this._deviceTrackingStatus)) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
// userId -> true
|
||||
// userId -> promise
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
// deferred which is resolved when the current device query resolves.
|
||||
// (null if there is no current request).
|
||||
this._currentQueryDeferred = null;
|
||||
|
||||
// deferred which is resolved when the *next* device query resolves.
|
||||
//
|
||||
// Normally it is meaningless for this to be non-null when
|
||||
// _currentQueryDeferred is null, but it can happen if the previous
|
||||
// query has finished but the next one has not yet started (because the
|
||||
// previous query failed, in which case we deliberately delay starting
|
||||
// the next query to avoid tight-looping).
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
this.lastKnownSyncToken = null;
|
||||
}
|
||||
|
||||
@@ -68,43 +68,27 @@ export default class DeviceList {
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
downloadKeys(userIds, forceDownload) {
|
||||
let needsRefresh = false;
|
||||
let waitForCurrentQuery = false;
|
||||
const usersToDownload = [];
|
||||
const promises = [];
|
||||
|
||||
userIds.forEach((u) => {
|
||||
if (this._pendingUsersWithNewDevices[u]) {
|
||||
// we already know this user's devices are outdated
|
||||
needsRefresh = true;
|
||||
} else if (this._keyDownloadsInProgressByUser[u]) {
|
||||
// already a download in progress - just wait for it.
|
||||
// (even if forceDownload is true)
|
||||
waitForCurrentQuery = true;
|
||||
} else if (forceDownload) {
|
||||
console.log("Invalidating device list for " + u +
|
||||
" for forceDownload");
|
||||
this.invalidateUserDeviceList(u);
|
||||
needsRefresh = true;
|
||||
} else if (!this.getStoredDevicesForUser(u)) {
|
||||
console.log("Invalidating device list for " + u +
|
||||
" due to empty cache");
|
||||
this.invalidateUserDeviceList(u);
|
||||
needsRefresh = true;
|
||||
const trackingStatus = this._deviceTrackingStatus[u];
|
||||
if (this._keyDownloadsInProgressByUser[u]) {
|
||||
// already a key download in progress/queued for this user; its results
|
||||
// will be good enough for us.
|
||||
promises.push(this._keyDownloadsInProgressByUser[u]);
|
||||
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
|
||||
usersToDownload.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
let promise;
|
||||
if (needsRefresh) {
|
||||
console.log("downloadKeys: waiting for next key query");
|
||||
promise = this._startOrQueueDeviceQuery();
|
||||
} else if(waitForCurrentQuery) {
|
||||
console.log("downloadKeys: waiting for in-flight query to complete");
|
||||
promise = this._currentQueryDeferred.promise;
|
||||
} else {
|
||||
// we're all up-to-date.
|
||||
promise = q();
|
||||
if (usersToDownload.length != 0) {
|
||||
console.log("downloadKeys: downloading for", usersToDownload);
|
||||
const downloadPromise = this._doKeyDownload(usersToDownload);
|
||||
promises.push(downloadPromise);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return q.all(promises).then(() => {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
});
|
||||
}
|
||||
@@ -216,14 +200,15 @@ export default class DeviceList {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the cached device list for the given user outdated.
|
||||
* flag the given user for device-list tracking, if they are not already.
|
||||
*
|
||||
* This doesn't set off an update, so that several users can be batched
|
||||
* together. Call refreshOutdatedDeviceLists() for that.
|
||||
* This will mean that a subsequent call to refreshOutdatedDeviceLists()
|
||||
* will download the device list for the user, and that subsequent calls to
|
||||
* invalidateUserDeviceList will trigger more updates.
|
||||
*
|
||||
* @param {String} userId
|
||||
*/
|
||||
invalidateUserDeviceList(userId) {
|
||||
startTrackingDeviceList(userId) {
|
||||
// sanity-check the userId. This is mostly paranoia, but if synapse
|
||||
// can't parse the userId we give it as an mxid, it 500s the whole
|
||||
// request and we can never update the device lists again (because
|
||||
@@ -234,124 +219,229 @@ export default class DeviceList {
|
||||
if (typeof userId !== 'string') {
|
||||
throw new Error('userId must be a string; was '+userId);
|
||||
}
|
||||
this._pendingUsersWithNewDevices[userId] = true;
|
||||
if (!this._deviceTrackingStatus[userId]) {
|
||||
console.log('Now tracking device list for ' + userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; instead we wait for the forthcoming
|
||||
// refreshOutdatedDeviceLists.
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is not already a device list query in progress, and we have
|
||||
* users who have outdated device lists, start a query now.
|
||||
* Mark the cached device list for the given user outdated.
|
||||
*
|
||||
* If we are not tracking this user's devices, we'll do nothing. Otherwise
|
||||
* we flag the user as needing an update.
|
||||
*
|
||||
* This doesn't actually set off an update, so that several users can be
|
||||
* batched together. Call refreshOutdatedDeviceLists() for that.
|
||||
*
|
||||
* @param {String} userId
|
||||
*/
|
||||
invalidateUserDeviceList(userId) {
|
||||
if (this._deviceTrackingStatus[userId]) {
|
||||
console.log("Marking device list outdated for", userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; instead we wait for the forthcoming
|
||||
// refreshOutdatedDeviceLists.
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all tracked device lists as outdated.
|
||||
*
|
||||
* This will flag each user whose devices we are tracking as in need of an
|
||||
* update.
|
||||
*/
|
||||
invalidateAllDeviceLists() {
|
||||
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
||||
this.invalidateUserDeviceList(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have users who have outdated device lists, start key downloads for them
|
||||
*
|
||||
* @returns {Promise} which completes when the download completes; normally there
|
||||
* is no need to wait for this (it's mostly for the unit tests).
|
||||
*/
|
||||
refreshOutdatedDeviceLists() {
|
||||
if (this._currentQueryDeferred) {
|
||||
// request already in progress - do nothing. (We will automatically
|
||||
// make another request if there are more users with outdated
|
||||
// device lists when the current request completes).
|
||||
const usersToDownload = [];
|
||||
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
||||
const stat = this._deviceTrackingStatus[userId];
|
||||
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
|
||||
usersToDownload.push(userId);
|
||||
}
|
||||
}
|
||||
if (usersToDownload.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._startDeviceQuery();
|
||||
// we didn't persist the tracking status during
|
||||
// invalidateUserDeviceList, so do it now.
|
||||
this._persistDeviceTrackingStatus();
|
||||
|
||||
return this._doKeyDownload(usersToDownload);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fire off download update requests for the given users, and update the
|
||||
* device list tracking status for them, and the
|
||||
* _keyDownloadsInProgressByUser map for them.
|
||||
*
|
||||
* @param {String[]} users list of userIds
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
_doKeyDownload(users) {
|
||||
if (users.length === 0) {
|
||||
// nothing to do
|
||||
return q();
|
||||
}
|
||||
|
||||
const prom = this._serialiser.updateDevicesForUsers(
|
||||
users, this.lastKnownSyncToken,
|
||||
).then(() => {
|
||||
finished(true);
|
||||
}, (e) => {
|
||||
console.error(
|
||||
'Error downloading keys for ' + users + ":", e,
|
||||
);
|
||||
finished(false);
|
||||
throw e;
|
||||
});
|
||||
|
||||
users.forEach((u) => {
|
||||
this._keyDownloadsInProgressByUser[u] = prom;
|
||||
const stat = this._deviceTrackingStatus[u];
|
||||
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS;
|
||||
}
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
users.forEach((u) => {
|
||||
// we may have queued up another download request for this user
|
||||
// since we started this request. If that happens, we should
|
||||
// ignore the completion of the first one.
|
||||
if (this._keyDownloadsInProgressByUser[u] !== prom) {
|
||||
console.log('Another update in the queue for', u,
|
||||
'- not marking up-to-date');
|
||||
return;
|
||||
}
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
const stat = this._deviceTrackingStatus[u];
|
||||
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
if (success) {
|
||||
// we didn't get any new invalidations since this download started:
|
||||
// this user's device list is now up to date.
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
|
||||
console.log("Device list for", u, "now up to date");
|
||||
} else {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._persistDeviceTrackingStatus();
|
||||
};
|
||||
|
||||
return prom;
|
||||
}
|
||||
|
||||
_persistDeviceTrackingStatus() {
|
||||
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialises updates to device lists
|
||||
*
|
||||
* Ensures that results from /keys/query are not overwritten if a second call
|
||||
* completes *before* an earlier one.
|
||||
*
|
||||
* It currently does this by ensuring only one call to /keys/query happens at a
|
||||
* time (and queuing other requests up).
|
||||
*/
|
||||
class DeviceListUpdateSerialiser {
|
||||
constructor(baseApis, sessionStore, olmDevice) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._olmDevice = olmDevice;
|
||||
|
||||
this._downloadInProgress = false;
|
||||
|
||||
// users which are queued for download
|
||||
// userId -> true
|
||||
this._keyDownloadsQueuedByUser = {};
|
||||
|
||||
// deferred which is resolved when the queued users are downloaded.
|
||||
//
|
||||
// non-null indicates that we have users queued for download.
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
// sync token to be used for the next query: essentially the
|
||||
// most recent one we know about
|
||||
this._nextSyncToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is currently a device list query in progress, returns a promise
|
||||
* which will resolve when the *next* query completes. Otherwise, starts
|
||||
* a new query, and returns a promise which resolves when it completes.
|
||||
* Make a key query request for the given users
|
||||
*
|
||||
* @return {Promise}
|
||||
* @param {String[]} users list of user ids
|
||||
*
|
||||
* @param {String} syncToken sync token to pass in the query request, to
|
||||
* help the HS give the most recent results
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
_startOrQueueDeviceQuery() {
|
||||
if (!this._currentQueryDeferred) {
|
||||
this._startDeviceQuery();
|
||||
if (!this._currentQueryDeferred) {
|
||||
return q();
|
||||
}
|
||||
|
||||
return this._currentQueryDeferred.promise;
|
||||
}
|
||||
updateDevicesForUsers(users, syncToken) {
|
||||
users.forEach((u) => {
|
||||
this._keyDownloadsQueuedByUser[u] = true;
|
||||
});
|
||||
this._nextSyncToken = syncToken;
|
||||
|
||||
if (!this._queuedQueryDeferred) {
|
||||
this._queuedQueryDeferred = q.defer();
|
||||
}
|
||||
|
||||
return this._queuedQueryDeferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* kick off a new device query
|
||||
*
|
||||
* Throws if there is already a query in progress.
|
||||
*/
|
||||
_startDeviceQuery() {
|
||||
if (this._currentQueryDeferred) {
|
||||
throw new Error("DeviceList._startDeviceQuery called with request active");
|
||||
if (this._downloadInProgress) {
|
||||
// just queue up these users
|
||||
console.log('Queued key download for', users);
|
||||
return this._queuedQueryDeferred.promise;
|
||||
}
|
||||
|
||||
this._currentQueryDeferred = this._queuedQueryDeferred || q.defer();
|
||||
// start a new download.
|
||||
return this._doQueuedQueries();
|
||||
}
|
||||
|
||||
_doQueuedQueries() {
|
||||
if (this._downloadInProgress) {
|
||||
throw new Error(
|
||||
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser);
|
||||
this._keyDownloadsQueuedByUser = {};
|
||||
const deferred = this._queuedQueryDeferred;
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
const users = Object.keys(this._pendingUsersWithNewDevices);
|
||||
if (users.length === 0) {
|
||||
// nothing to do
|
||||
this._currentQueryDeferred.resolve();
|
||||
this._currentQueryDeferred = null;
|
||||
console.log('Starting key download for', downloadUsers);
|
||||
this._downloadInProgress = true;
|
||||
|
||||
// that means we're up-to-date with the lastKnownSyncToken.
|
||||
const token = this.lastKnownSyncToken;
|
||||
if (token !== null) {
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(token);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._doKeyDownloadForUsers(users).done(() => {
|
||||
users.forEach((u) => {
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
});
|
||||
|
||||
this._currentQueryDeferred.resolve();
|
||||
this._currentQueryDeferred = null;
|
||||
|
||||
// flush out any more requests that were blocked up while that
|
||||
// was going on.
|
||||
this._startDeviceQuery();
|
||||
}, (e) => {
|
||||
console.error(
|
||||
'Error updating device key cache for ' + users + ":", e,
|
||||
);
|
||||
|
||||
// reinstate the pending flags on any users which failed; this will
|
||||
// mean that we will do another download in the future (actually on
|
||||
// the next /sync).
|
||||
users.forEach((u) => {
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
this._pendingUsersWithNewDevices[u] = true;
|
||||
});
|
||||
|
||||
this._currentQueryDeferred.reject(e);
|
||||
this._currentQueryDeferred = null;
|
||||
});
|
||||
|
||||
users.forEach((u) => {
|
||||
delete this._pendingUsersWithNewDevices[u];
|
||||
this._keyDownloadsInProgressByUser[u] = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} downloadUsers list of userIds
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
_doKeyDownloadForUsers(downloadUsers) {
|
||||
console.log('Starting key download for ' + downloadUsers);
|
||||
|
||||
const token = this.lastKnownSyncToken;
|
||||
const opts = {};
|
||||
if (token) {
|
||||
opts.token = token;
|
||||
if (this._nextSyncToken) {
|
||||
opts.token = this._nextSyncToken;
|
||||
}
|
||||
return this._baseApis.downloadKeysForUsers(
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
@@ -369,12 +459,23 @@ export default class DeviceList {
|
||||
}
|
||||
|
||||
return prom;
|
||||
}).then(() => {
|
||||
if (token !== null) {
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(token);
|
||||
}
|
||||
}).done(() => {
|
||||
console.log('Completed key download for ' + downloadUsers);
|
||||
|
||||
this._downloadInProgress = false;
|
||||
deferred.resolve();
|
||||
|
||||
// if we have queued users, fire off another request.
|
||||
if (this._queuedQueryDeferred) {
|
||||
this._doQueuedQueries();
|
||||
}
|
||||
}, (e) => {
|
||||
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||
this._downloadInProgressInProgress = false;
|
||||
deferred.reject(e);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
_processQueryResponseForUser(userId, response) {
|
||||
|
||||
+46
-65
@@ -61,7 +61,7 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
|
||||
this._initialDeviceListInvalidationDone = false;
|
||||
this._initialDeviceListInvalidationPending = false;
|
||||
|
||||
this._clientRunning = false;
|
||||
|
||||
@@ -558,9 +558,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
* Configure a room to use encryption (ie, save a flag in the sessionstore).
|
||||
*
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
*
|
||||
* @param {object} config The encryption config for the room.
|
||||
*
|
||||
* @param {boolean=} inhibitDeviceQuery true to suppress device list query for
|
||||
* users in the room (for now)
|
||||
*/
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config, inhibitDeviceQuery) {
|
||||
// if we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
const existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||
@@ -590,21 +594,16 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
});
|
||||
this._roomEncryptors[roomId] = alg;
|
||||
|
||||
// if encryption was not previously enabled in this room, we will have been
|
||||
// ignoring new device events for these users so far. We may well have
|
||||
// up-to-date lists for some users, for instance if we were sharing other
|
||||
// e2e rooms with them, so there is room for optimisation here, but for now
|
||||
// we just invalidate everyone in the room.
|
||||
if (!existingConfig) {
|
||||
console.log("Enabling encryption in " + roomId + " for the first time; " +
|
||||
"invalidating device lists for all users therein");
|
||||
const room = this._clientStore.getRoom(roomId);
|
||||
const members = room.getJoinedMembers();
|
||||
members.forEach((m) => {
|
||||
this._deviceList.invalidateUserDeviceList(m.userId);
|
||||
});
|
||||
// the actual refresh happens once we've finished processing the sync,
|
||||
// in _onSyncCompleted.
|
||||
// make sure we are tracking the device lists for all users in this room.
|
||||
console.log("Enabling encryption in " + roomId + "; " +
|
||||
"starting to track device lists for all users therein");
|
||||
const room = this._clientStore.getRoom(roomId);
|
||||
const members = room.getJoinedMembers();
|
||||
members.forEach((m) => {
|
||||
this._deviceList.startTrackingDeviceList(m.userId);
|
||||
});
|
||||
if (!inhibitDeviceQuery) {
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -795,7 +794,9 @@ Crypto.prototype._onCryptoEvent = function(event) {
|
||||
const content = event.getContent();
|
||||
|
||||
try {
|
||||
this.setRoomEncryption(roomId, content);
|
||||
// inhibit the device list refresh for now - it will happen once we've
|
||||
// finished processing the sync, in _onSyncCompleted.
|
||||
this.setRoomEncryption(roomId, content, true);
|
||||
} catch (e) {
|
||||
console.error("Error configuring encryption in room " + roomId +
|
||||
":", e);
|
||||
@@ -814,6 +815,8 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
|
||||
const nextSyncToken = syncData.nextSyncToken;
|
||||
|
||||
if (!syncData.oldSyncToken) {
|
||||
console.log("Completed initial sync");
|
||||
|
||||
// an initialsync.
|
||||
this._sendNewDeviceEvents();
|
||||
|
||||
@@ -821,37 +824,40 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
|
||||
// invalidate devices which have changed since then.
|
||||
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
|
||||
if (oldSyncToken !== null) {
|
||||
this._initialDeviceListInvalidationPending = true;
|
||||
this._invalidateDeviceListsSince(
|
||||
oldSyncToken, nextSyncToken,
|
||||
).catch((e) => {
|
||||
// if that failed, we fall back to invalidating everyone.
|
||||
console.warn("Error fetching changed device list", e);
|
||||
this._invalidateDeviceListForAllActiveUsers();
|
||||
this._deviceList.invalidateAllDeviceLists();
|
||||
}).done(() => {
|
||||
this._initialDeviceListInvalidationDone = true;
|
||||
this._initialDeviceListInvalidationPending = false;
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
});
|
||||
} else {
|
||||
// otherwise, we have to invalidate all devices for all users we
|
||||
// share a room with.
|
||||
// are tracking.
|
||||
console.log("Completed first initialsync; invalidating all " +
|
||||
"device list caches");
|
||||
this._invalidateDeviceListForAllActiveUsers();
|
||||
this._initialDeviceListInvalidationDone = true;
|
||||
this._deviceList.invalidateAllDeviceLists();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._initialDeviceListInvalidationDone) {
|
||||
// if we've got an up-to-date list of users with outdated device lists,
|
||||
// tell the device list about the new sync token (but not otherwise, because
|
||||
// otherwise we'll start thinking we're more in sync than we are.)
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
if (!this._initialDeviceListInvalidationPending) {
|
||||
// we can now store our sync token so that we can get an update on
|
||||
// restart rather than having to invalidate everyone.
|
||||
//
|
||||
// (we don't really need to do this on every sync - we could just
|
||||
// do it periodically)
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
|
||||
}
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
|
||||
// we don't start uploading one-time keys until we've caught up with
|
||||
// to-device messages, to help us avoid throwing away one-time-keys that we
|
||||
// are about to receive messages for
|
||||
@@ -928,49 +934,18 @@ Crypto.prototype._invalidateDeviceListsSince = function(
|
||||
return this._baseApis.getKeyChanges(
|
||||
oldSyncToken, lastKnownSyncToken,
|
||||
).then((r) => {
|
||||
console.log("got key changes since", oldSyncToken, ":", r.changed);
|
||||
|
||||
if (!r.changed || !Array.isArray(r.changed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only invalidate users we share an e2e room with - we don't
|
||||
// care about users in non-e2e rooms.
|
||||
const filteredUserIds = this._getE2eRoomMembers();
|
||||
r.changed.forEach((u) => {
|
||||
if (u in filteredUserIds) {
|
||||
this._deviceList.invalidateUserDeviceList(u);
|
||||
}
|
||||
this._deviceList.invalidateUserDeviceList(u);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate any stored device list for any users we share an e2e room with
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._invalidateDeviceListForAllActiveUsers = function() {
|
||||
Object.keys(this._getE2eRoomMembers()).forEach((m) => {
|
||||
this._deviceList.invalidateUserDeviceList(m);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* get the users we share an e2e-enabled room with
|
||||
*
|
||||
* @returns {Object<string>} userid->userid map (should be a Set but argh ES6)
|
||||
*/
|
||||
Crypto.prototype._getE2eRoomMembers = function() {
|
||||
const userIds = Object.create(null);
|
||||
|
||||
const rooms = this._getE2eRooms();
|
||||
for (const r of rooms) {
|
||||
const members = r.getJoinedMembers();
|
||||
members.forEach((m) => { userIds[m.userId] = m.userId; });
|
||||
}
|
||||
|
||||
return userIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of the e2e-enabled rooms we are members of
|
||||
*
|
||||
@@ -1039,6 +1014,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.membership == 'join') {
|
||||
console.log('Join event for ' + member.userId + ' in ' + roomId);
|
||||
// make sure we are tracking the deviceList for this user
|
||||
this._deviceList.startTrackingDeviceList(member.userId);
|
||||
}
|
||||
|
||||
alg.onRoomMembership(event, member, oldMembership);
|
||||
};
|
||||
|
||||
|
||||
@@ -318,6 +318,13 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
this._currentStage = nextStage;
|
||||
|
||||
if (nextStage == 'm.login.dummy') {
|
||||
this.submitAuthDict({
|
||||
type: 'm.login.dummy',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._data.errcode || this._data.error) {
|
||||
this._stateUpdatedCallback(nextStage, {
|
||||
errcode: this._data.errcode || "",
|
||||
|
||||
@@ -73,6 +73,23 @@ module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
*/
|
||||
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
|
||||
|
||||
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput;
|
||||
|
||||
|
||||
// expose the underlying request object so different environments can use
|
||||
// different request libs (e.g. request or browser-request)
|
||||
let request;
|
||||
|
||||
@@ -99,6 +99,14 @@ WebStorageSessionStore.prototype = {
|
||||
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
|
||||
},
|
||||
|
||||
storeEndToEndDeviceTrackingStatus: function(statusMap) {
|
||||
setJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS, statusMap);
|
||||
},
|
||||
|
||||
getEndToEndDeviceTrackingStatus: function() {
|
||||
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the sync token corresponding to the device list.
|
||||
*
|
||||
@@ -202,6 +210,7 @@ WebStorageSessionStore.prototype = {
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
|
||||
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
|
||||
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
|
||||
+123
-64
@@ -91,7 +91,7 @@ function MatrixCall(opts) {
|
||||
utils.checkObjectHasKeys(server, ["urls"]);
|
||||
});
|
||||
|
||||
this.callId = "c" + new Date().getTime();
|
||||
this.callId = "c" + new Date().getTime() + Math.random();
|
||||
this.state = 'fledgling';
|
||||
this.didConnect = false;
|
||||
|
||||
@@ -155,7 +155,7 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
|
||||
/**
|
||||
* Place a screen-sharing call to this room. This includes audio.
|
||||
* <b>This method is EXPERIMENTAL and subject to change without warning. It
|
||||
* only works in Google Chrome.</b>
|
||||
* only works in Google Chrome and Firefox >= 44.</b>
|
||||
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></code> DOM element
|
||||
@@ -166,7 +166,7 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
function(remoteVideoElement, localVideoElement) {
|
||||
debuglog("placeScreenSharingCall");
|
||||
checkForErrorListener(this);
|
||||
const screenConstraints = _getChromeScreenSharingConstraints(this);
|
||||
const screenConstraints = _getScreenSharingConstraints(this);
|
||||
if (!screenConstraints) {
|
||||
return;
|
||||
}
|
||||
@@ -335,6 +335,7 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||||
this.remoteVideoElement.muted = true;
|
||||
this.remoteAudioElement = element;
|
||||
this.remoteAudioElement.muted = false;
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
};
|
||||
|
||||
@@ -409,12 +410,12 @@ MatrixCall.prototype.answer = function() {
|
||||
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
||||
this.webRtc.getUserMedia(
|
||||
_getUserMediaVideoContraints(this.type),
|
||||
hookCallback(self, self._gotUserMediaForAnswer),
|
||||
hookCallback(self, self._getUserMediaFailed),
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
);
|
||||
setState(this, 'wait_local_media');
|
||||
} else if (this.localAVStream) {
|
||||
this._gotUserMediaForAnswer(this.localAVStream);
|
||||
this._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
} else if (this.waitForLocalAVStream) {
|
||||
setState(this, 'wait_local_media');
|
||||
}
|
||||
@@ -433,11 +434,11 @@ MatrixCall.prototype._replacedBy = function(newCall) {
|
||||
newCall.waitForLocalAVStream = true;
|
||||
} else if (this.state == 'create_offer') {
|
||||
debuglog("Handing local stream to new call");
|
||||
newCall._gotUserMediaForAnswer(this.localAVStream);
|
||||
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
} else if (this.state == 'invite_sent') {
|
||||
debuglog("Handing local stream to new call");
|
||||
newCall._gotUserMediaForAnswer(this.localAVStream);
|
||||
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
}
|
||||
newCall.localVideoElement = this.localVideoElement;
|
||||
@@ -523,48 +524,69 @@ MatrixCall.prototype.isMicrophoneMuted = function() {
|
||||
* @private
|
||||
* @param {Object} stream
|
||||
*/
|
||||
MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
|
||||
if (this.successor) {
|
||||
this.successor._gotUserMediaForAnswer(stream);
|
||||
this.successor._maybeGotUserMediaForAnswer(stream);
|
||||
return;
|
||||
}
|
||||
if (this.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
debuglog("_gotUserMediaForInvite -> " + this.type);
|
||||
debuglog("_maybeGotUserMediaForInvite -> " + this.type);
|
||||
const self = this;
|
||||
const videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
|
||||
} else {
|
||||
this.assignElement(videoEl, stream, "localVideo");
|
||||
}
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
const error = stream;
|
||||
const constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': self.type === 'video',
|
||||
},
|
||||
};
|
||||
if (stream instanceof MediaStream) {
|
||||
const videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video" +
|
||||
" element");
|
||||
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
|
||||
} else {
|
||||
this.assignElement(videoEl, stream, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.screenSharingStream) {
|
||||
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
|
||||
stream = this.screenSharingStream;
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
} else if (error.name === 'PermissionDeniedError') {
|
||||
debuglog('User denied access to camera/microphone.' +
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.screenSharingStream) {
|
||||
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
|
||||
stream = this.screenSharingStream;
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
this.peerConn.createOffer(
|
||||
hookCallback(self, self._gotLocalOffer),
|
||||
hookCallback(self, self._getLocalOfferFailed),
|
||||
constraints,
|
||||
);
|
||||
setState(self, 'create_offer');
|
||||
};
|
||||
@@ -574,33 +596,44 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
* @private
|
||||
* @param {Object} stream
|
||||
*/
|
||||
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||||
MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
|
||||
const self = this;
|
||||
if (self.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
const localVidEl = self.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
this.assignElement(localVidEl, stream, "localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
const error = stream;
|
||||
if (stream instanceof MediaStream) {
|
||||
const localVidEl = self.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
this.assignElement(localVidEl, stream, "localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
} else if (error.name === 'PermissionDeniedError') {
|
||||
debuglog('User denied access to camera/microphone.' +
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
|
||||
const constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': self.type == 'video',
|
||||
'OfferToReceiveVideo': self.type === 'video',
|
||||
},
|
||||
};
|
||||
self.peerConn.createAnswer(function(description) {
|
||||
@@ -1126,8 +1159,8 @@ const _placeCallWithConstraints = function(self, constraints) {
|
||||
self.client.callList[self.callId] = self;
|
||||
self.webRtc.getUserMedia(
|
||||
constraints,
|
||||
hookCallback(self, self._gotUserMediaForInvite),
|
||||
hookCallback(self, self._getUserMediaFailed),
|
||||
hookCallback(self, self._maybeGotUserMediaForInvite),
|
||||
hookCallback(self, self._maybeGotUserMediaForInvite),
|
||||
);
|
||||
setState(self, 'wait_local_media');
|
||||
self.direction = 'outbound';
|
||||
@@ -1160,7 +1193,7 @@ const _createPeerConnection = function(self) {
|
||||
return pc;
|
||||
};
|
||||
|
||||
const _getChromeScreenSharingConstraints = function(call) {
|
||||
const _getScreenSharingConstraints = function(call) {
|
||||
const screen = global.screen;
|
||||
if (!screen) {
|
||||
call.emit("error", callError(
|
||||
@@ -1172,6 +1205,7 @@ const _getChromeScreenSharingConstraints = function(call) {
|
||||
|
||||
return {
|
||||
video: {
|
||||
mediaSource: 'screen',
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
chromeMediaSourceId: "" + Date.now(),
|
||||
@@ -1187,16 +1221,25 @@ const _getChromeScreenSharingConstraints = function(call) {
|
||||
const _getUserMediaVideoContraints = function(callType) {
|
||||
switch (callType) {
|
||||
case 'voice':
|
||||
return ({audio: true, video: false});
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
}, video: false,
|
||||
};
|
||||
case 'video':
|
||||
return ({audio: true, video: {
|
||||
mandatory: {
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
minHeight: 360,
|
||||
maxHeight: 360,
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
}, video: {
|
||||
deviceId: videoInput ? {exact: videoInput} : undefined,
|
||||
mandatory: {
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
minHeight: 360,
|
||||
maxHeight: 360,
|
||||
},
|
||||
},
|
||||
}});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1228,6 +1271,22 @@ const forAllTracksOnStream = function(s, f) {
|
||||
/** The MatrixCall class. */
|
||||
module.exports.MatrixCall = MatrixCall;
|
||||
|
||||
let audioInput;
|
||||
let videoInput;
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setAudioInput = function(deviceId) { audioInput = deviceId; };
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
|
||||
|
||||
/**
|
||||
* Create a new Matrix call for the browser.
|
||||
|
||||
Reference in New Issue
Block a user