Compare commits

..

66 Commits

Author SHA1 Message Date
RiotRobot 662fcb426b v9.0.0 2020-10-26 16:42:34 +00:00
RiotRobot fd0fe9e225 Prepare changelog for v9.0.0 2020-10-26 16:42:33 +00:00
David Baker 295c591e95 Merge pull request #1528 from matrix-org/dbkr/debugl_rel
Fix logger typo
2020-10-22 16:06:31 +01:00
David Baker 0cbe35e41f Fix logger typo 2020-10-22 16:00:23 +01:00
RiotRobot d4e107b3cd v9.0.0-rc.1 2020-10-21 14:33:50 +01:00
RiotRobot 881b60c85f Prepare changelog for v9.0.0-rc.1 2020-10-21 14:33:50 +01:00
David Baker e3a8631faa Merge pull request #1521 from matrix-org/dbkr/dont_cache_versions_failure
Don't cache failures when fetching /versions
2020-10-20 16:35:49 +01:00
David Baker 3eab51ce79 Don't cache failures when fetching /versions
Otherwise we never recover

Fixes https://github.com/vector-im/element-web/issues/15509
2020-10-20 16:25:50 +01:00
J. Ryan Stinnett 9db0fe0795 Merge pull request #1518 from matrix-org/jryans/release-install-first
Install deps first as part of release
2020-10-20 15:45:25 +01:00
Michael Telatynski 228af037e3 Merge pull request #1517 from matrix-org/t3chguy/fix/js-1504
[Breaking] Change hasPendingEvent to return false if pending ordering !detached
2020-10-20 15:28:29 +01:00
J. Ryan Stinnett 654f250fd8 Merge pull request #1519 from matrix-org/jryans/release-disable-editor
Skip editor prompts for merges
2020-10-20 12:06:34 +01:00
J. Ryan Stinnett a8693d9d68 Skip editor prompts for merges
The merges are never edited, and even with this change they will still abort if
there are conflicts.
2020-10-20 11:47:59 +01:00
J. Ryan Stinnett f9f345e428 Move cache clean above install 2020-10-20 11:42:28 +01:00
J. Ryan Stinnett e678706414 Install deps first as part of release
This ensures we always install (without running build scripts) as the first step
of release process, as otherwise it may fail later due to mismatched types or
any number of other errors.
2020-10-20 11:20:57 +01:00
Michael Telatynski 8018259480 Change hasPendingEvent to return false if pending event ordering !detached
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-19 14:56:58 +01:00
David Baker 9f713781cd Merge pull request #1516 from matrix-org/dbkr/aint_no_party_like_a_typescript_party
Convert call test to TypeScript
2020-10-19 09:57:52 +01:00
David Baker 48d56dc5c0 Merge pull request #1512 from matrix-org/dbkr/party_in_the_usa
Support party_id
2020-10-19 09:57:30 +01:00
David Baker 701dfa09b4 Merge pull request #1510 from matrix-org/dbkr/hang_up_your_hangups
Support m.call.reject
2020-10-19 09:57:00 +01:00
David Baker d965648fd7 Convert call test to TypeScript
Typescript tests basically just appear to work, apart from needing
the jest types imported so the typescript checker knows what's what.

DConvert the webrtc test to typescript, which actually mostly just
serves to point out that we're not mocking the whole of `document`,
but oh well.
2020-10-16 18:20:03 +01:00
David Baker 08c15e7203 Merge pull request #1515 from matrix-org/dbkr/remove_specbuild
Remove specbuild from .gitignore
2020-10-16 17:56:02 +01:00
David Baker 9b9e52d0a2 Remove specbuild from .gitignore
It's no longer a thing
2020-10-16 17:47:31 +01:00
David Baker 38cc8fe7dc Merge pull request #1514 from matrix-org/dbkr/log_candidate_send_error
Log the error when we failed to send candidates
2020-10-16 16:52:05 +01:00
David Baker 590fac0fa9 Log the error when we failed to send candidates 2020-10-16 16:46:07 +01:00
David Baker 9590c8aaf0 Don't ignore hangups if we don't have a partner 2020-10-16 14:37:50 +01:00
David Baker e2b79e4e7e linty lint lint 2020-10-16 13:02:47 +01:00
David Baker 2df588f95a Support party_id
Send party_id on events and check the party_id of incoming events matches

Includes a basic test to assert that it actually does: we should
build out a decent test suite for calls as there's a lot of edge-case
functionality that can break and slip through the cracks (eg. glare).
This is a start.

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/1511
2020-10-16 12:53:08 +01:00
David Baker 7c3af91b42 Support m.call.reject
Start the migration to v1 VoIP by supporting m.call.reject, which
we'll send if the caller says they're v1. Our version stays as v0
for now, until we speak the rest of v1.

Honour the default reaosn in a hangup being user_hangup.
2020-10-15 14:51:05 +01:00
David Baker 4cc4b28c47 Merge pull request #1503 from matrix-org/dbkr/call_state_machine
Fixes for call state machine
2020-10-13 10:39:10 +01:00
David Baker fad9b4c67f Merge pull request #1506 from matrix-org/dbkr/fix_event_listener_remove
Fix call event handler listener removing
2020-10-12 14:17:53 +01:00
David Baker ade3b3a021 Fix call event handler listener removing
Wrong kind of event emitter...
2020-10-12 14:12:34 +01:00
RiotRobot d8c4101fdd Merge branch 'master' into develop 2020-10-12 13:18:23 +01:00
RiotRobot a12c250f2b v8.5.0 2020-10-12 13:15:07 +01:00
RiotRobot 57eef2d832 Prepare changelog for v8.5.0 2020-10-12 13:15:06 +01:00
David Baker b67a179a54 Merge pull request #1501 from matrix-org/dbkr/set_type_based_on_tracks
Set the type of the call based on the tracks
2020-10-12 11:42:40 +01:00
David Baker 06044b39c3 Merge pull request #1499 from matrix-org/dbkr/new_age_calls
Use new local timestamp for calls
2020-10-12 11:42:28 +01:00
J. Ryan Stinnett d16cf26c5f Merge pull request #1502 from matrix-org/jryans/sso-4s-integration
Adjust types and APIs to match React SDK
2020-10-12 11:41:06 +01:00
J. Ryan Stinnett b060c5af38 Tune crypto types 2020-10-12 11:01:00 +01:00
David Baker b28bad651e Make events names an enum 2020-10-12 10:15:58 +01:00
David Baker 5c4b7a3213 Add user_hangup error code
but special case it for now & don't send it: it needs voip v1
2020-10-09 18:51:35 +01:00
David Baker 7f21c591ae Fixes for call state machine
* Set 'connecting' state before sending answer, otherwise it can
   race with ICE connecting
 * Ignore completed ice connection state: connected is what we care about
 * Null-check remotestream when stopping media
 * Comments
2020-10-09 18:45:45 +01:00
David Baker 65b24f595c Missed a reference to callList 2020-10-09 18:14:00 +01:00
David Baker 9d9c2720c2 Hopefully make if statement clearer 2020-10-09 17:43:40 +01:00
David Baker f845100062 add 'public' to public method 2020-10-09 17:43:34 +01:00
David Baker e6155f9e37 Use MatrixClient as type
and different array syntax
2020-10-09 17:43:28 +01:00
J. Ryan Stinnett e9590e9093 Adjust types and APIs to match React SDK
Various small tweaks and alignments to match React SDK as part of TypeScript
conversion.

Part of https://github.com/vector-im/element-web/issues/15350
2020-10-09 17:21:14 +01:00
David Baker 49f2d1501c Set the type of the call based on the tracks
Remove the old hack of inspecting the SDP which no longer seems to
be necessary.
2020-10-08 15:42:39 +01:00
J. Ryan Stinnett c6819e0450 Omit stack trace if rehydration fails 2020-10-08 15:11:46 +01:00
David Baker f518ea95f4 Use getLocalAge() & add grace period
Use the new local-age field for deciding whether a call is still
valid or not. Also add a grace period so we don't ring half a second
before the call becomes invalid.
2020-10-08 11:49:54 +01:00
David Baker 92c6332143 use more enums 2020-10-08 11:32:32 +01:00
David Baker 0df1a7da21 copyright header 2020-10-08 11:12:39 +01:00
David Baker 487a9c0967 Extract the call event handler out to its own class
and convert it to TypeScript
2020-10-08 11:10:20 +01:00
David Baker fb89761671 Merge pull request #1495 from matrix-org/dbkr/age_is_just_a_number
Make an accurate version of 'age' for events
2020-10-08 09:29:16 +01:00
David Baker d1d3ae074d Add tests & fix some bugs found by said tests 2020-10-07 18:29:33 +01:00
David Baker 7dedaf90c3 Add a test for the age mangling 2020-10-07 18:04:20 +01:00
David Baker 687b98a09d Don't add localTs if an event has no age 2020-10-07 17:39:19 +01:00
David Baker c6b2e9873c Merge pull request #1498 from matrix-org/dbkr/call_options_is_optional
Make 'options' parameter optional
2020-10-07 17:15:26 +01:00
David Baker 1e80491675 Make 'options' parameter optional
It's deprecated so generally nothing should be passing it
2020-10-07 17:08:21 +01:00
Michael Telatynski a727da9193 Merge pull request #1497 from matrix-org/t3chguy/fix/14804
Create a giant event type enum
2020-10-07 16:48:39 +01:00
Michael Telatynski 0b7754581a Update src/@types/event.ts
Co-authored-by: Travis Ralston <travpc@gmail.com>
2020-10-07 16:32:04 +01:00
David Baker 452e8ea385 Merge pull request #1494 from matrix-org/revert-1493-revert-1487-dbkr/tsify_call
Convert call.js to Typescript & update WebRTC APIs (re-apply)
2020-10-07 15:36:27 +01:00
David Baker a189de9a2e Add comment on mass event copying 2020-10-07 15:32:22 +01:00
David Baker 8632ca6e37 Merge remote-tracking branch 'origin/develop' into revert-1493-revert-1487-dbkr/tsify_call 2020-10-07 15:16:21 +01:00
Michael Telatynski 7529d2b638 Create a giant event type enum
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-07 09:57:56 +01:00
David Baker c8a18d51e6 Lint (& unused variable) 2020-10-06 17:45:21 +01:00
David Baker d77af1e67a Make an accurate version of 'age' for events
We've always had 'age' in events but it's never really been an
accurate representation of the event's age because we never did
anything with it. This transforms it into a local clock timestamp
when the event arives and when it comes out of the sync store, and
changes getLocalAge() to use it.

react-sdk doesn't appear to use getLocalAge() but any 3rd party apps
that do may notice a slight change in bahaviour.
2020-10-06 15:24:09 +01:00
David Baker 81c95224d1 Revert "Revert "Convert call.js to Typescript & update WebRTC APIs"" 2020-10-06 15:21:45 +01:00
23 changed files with 2326 additions and 1740 deletions
-1
View File
@@ -12,7 +12,6 @@ lib-cov
out
/dist
/lib
/specbuild
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
+63
View File
@@ -1,3 +1,66 @@
Changes in [9.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0) (2020-10-26)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0-rc.1...v9.0.0)
* Fix logger typo
[\#1528](https://github.com/matrix-org/matrix-js-sdk/pull/1528)
Changes in [9.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0-rc.1) (2020-10-21)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0...v9.0.0-rc.1)
BREAKING CHANGES
---
* `hasPendingEvent` now returns false instead of throwing when pending ordering mode is not `detached`
All changes
---
* Don't cache failures when fetching /versions
[\#1521](https://github.com/matrix-org/matrix-js-sdk/pull/1521)
* Install deps first as part of release
[\#1518](https://github.com/matrix-org/matrix-js-sdk/pull/1518)
* [Breaking] Change hasPendingEvent to return false if pending ordering
!detached
[\#1517](https://github.com/matrix-org/matrix-js-sdk/pull/1517)
* Skip editor prompts for merges
[\#1519](https://github.com/matrix-org/matrix-js-sdk/pull/1519)
* Convert call test to TypeScript
[\#1516](https://github.com/matrix-org/matrix-js-sdk/pull/1516)
* Support party_id
[\#1512](https://github.com/matrix-org/matrix-js-sdk/pull/1512)
* Support m.call.reject
[\#1510](https://github.com/matrix-org/matrix-js-sdk/pull/1510)
* Remove specbuild from .gitignore
[\#1515](https://github.com/matrix-org/matrix-js-sdk/pull/1515)
* Log the error when we failed to send candidates
[\#1514](https://github.com/matrix-org/matrix-js-sdk/pull/1514)
* Fixes for call state machine
[\#1503](https://github.com/matrix-org/matrix-js-sdk/pull/1503)
* Fix call event handler listener removing
[\#1506](https://github.com/matrix-org/matrix-js-sdk/pull/1506)
* Set the type of the call based on the tracks
[\#1501](https://github.com/matrix-org/matrix-js-sdk/pull/1501)
* Use new local timestamp for calls
[\#1499](https://github.com/matrix-org/matrix-js-sdk/pull/1499)
* Adjust types and APIs to match React SDK
[\#1502](https://github.com/matrix-org/matrix-js-sdk/pull/1502)
* Make an accurate version of 'age' for events
[\#1495](https://github.com/matrix-org/matrix-js-sdk/pull/1495)
* Make 'options' parameter optional
[\#1498](https://github.com/matrix-org/matrix-js-sdk/pull/1498)
* Create a giant event type enum
[\#1497](https://github.com/matrix-org/matrix-js-sdk/pull/1497)
* Convert call.js to Typescript & update WebRTC APIs (re-apply)
[\#1494](https://github.com/matrix-org/matrix-js-sdk/pull/1494)
Changes in [8.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0) (2020-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0-rc.1...v8.5.0)
* No changes since rc.1
Changes in [8.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0-rc.1) (2020-10-07)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.1...v8.5.0-rc.1)
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "8.5.0-rc.1",
"version": "9.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -14,7 +14,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 76 src spec",
"lint:js": "eslint --max-warnings 73 src spec",
"lint:types": "tsc --noEmit",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
@@ -67,6 +67,7 @@
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.11.5",
"@types/jest": "^26.0.14",
"@types/node": "12",
"@types/request": "^2.48.5",
"babel-eslint": "^10.1.0",
+10 -7
View File
@@ -94,6 +94,14 @@ if [ $# -ne 1 ]; then
exit 1
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
@@ -204,11 +212,6 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
yarn install
# We haven't tagged yet, so tell the dist script what version
# it's building
@@ -340,7 +343,7 @@ fi
echo "updating master branch"
git checkout master
git pull
git merge "$rel_branch"
git merge "$rel_branch" --no-edit
# push master to github
git push origin master
@@ -349,6 +352,6 @@ git push origin master
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
git checkout develop
git pull
git merge master
git merge master --no-edit
git push origin develop
fi
+7
View File
@@ -69,6 +69,9 @@ export function TestClient(
this.deviceKeys = null;
this.oneTimeKeys = {};
this._callEventHandler = {
calls: new Map(),
};
}
TestClient.prototype.toString = function() {
@@ -230,3 +233,7 @@ TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync completed`);
});
};
TestClient.prototype.isFallbackICEServerAllowed = function() {
return true;
};
+3 -3
View File
@@ -194,7 +194,7 @@ describe("Secrets", function() {
};
resetCrossSigningKeys(alice);
const newKeyId = await alice.addSecretStorageKey(
const { keyId: newKeyId } = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
);
// we don't await on this because it waits for the event to come down the sync
@@ -226,8 +226,8 @@ describe("Secrets", function() {
],
{
cryptoCallbacks: {
onSecretRequested: e => {
expect(e.name).toBe("foo");
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
expect(secretName).toBe("foo");
return "bar";
},
},
+66
View File
@@ -17,6 +17,39 @@ limitations under the License.
import {SyncAccumulator} from "../../src/sync-accumulator";
// The event body & unsigned object get frozen to assert that they don't get altered
// by the impl
const RES_WITH_AGE = {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
timeline: {
events: [
Object.freeze({
content: {
body: "This thing is happening right now!",
},
origin_server_ts: 123456789,
sender: "@alice:localhost",
type: "m.room.message",
unsigned: Object.freeze({
age: 50,
}),
}),
],
prev_batch: "something",
},
},
},
},
};
describe("SyncAccumulator", function() {
let sa;
@@ -368,6 +401,39 @@ describe("SyncAccumulator", function() {
expect(summary["m.joined_member_count"]).toEqual(5);
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
});
it("should return correctly adjusted age attributes", () => {
const delta = 1000;
const startingTs = 1000;
const oldDateNow = Date.now;
try {
Date.now = jest.fn();
Date.now.mockReturnValue(startingTs);
sa.accumulate(RES_WITH_AGE);
Date.now.mockReturnValue(startingTs + delta);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
);
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
);
} finally {
Date.now = oldDateNow;
}
});
it("should mangle age without adding extra keys", () => {
sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON();
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
);
});
});
});
+182
View File
@@ -0,0 +1,182 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {TestClient} from '../../TestClient';
import {MatrixCall} from '../../../src/webrtc/call';
const DUMMY_SDP = (
"v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
"a=ice-options:trickle\r\n" +
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
"a=setup:active\r\n" +
"a=mid:0\r\n" +
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
"a=sendrecv\r\n" +
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
"a=rtcp-mux\r\n" +
"a=rtpmap:111 opus/48000/2\r\n" +
"a=rtcp-fb:111 transport-cc\r\n" +
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
"a=rtpmap:103 ISAC/16000\r\n" +
"a=rtpmap:104 ISAC/32000\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:8 PCMA/8000\r\n" +
"a=rtpmap:106 CN/32000\r\n" +
"a=rtpmap:105 CN/16000\r\n" +
"a=rtpmap:13 CN/8000\r\n" +
"a=rtpmap:110 telephone-event/48000\r\n" +
"a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\r\n" +
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
class MockRTCPeerConnection {
localDescription: RTCSessionDescription;
constructor() {
this.localDescription = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() {},
};
}
addEventListener() {}
createOffer() {
return Promise.resolve({});
}
setRemoteDescription() {
return Promise.resolve();
}
setLocalDescription() {
return Promise.resolve();
}
}
describe('Call', function() {
let client;
let call;
let prevNavigator;
let prevDocument;
let prevWindow;
beforeEach(function() {
prevNavigator = global.navigator;
prevDocument = global.document;
prevWindow = global.window;
global.navigator = {
mediaDevices: {
// @ts-ignore Mock
getUserMedia: () => {
return {
getTracks: () => [],
getAudioTracks: () => [],
getVideoTracks: () => [],
};
},
},
};
global.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: {},
};
// @ts-ignore Mock
global.document = {};
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
call = new MatrixCall({
client: client.client,
roomId: '!foo:bar',
});
// call checks one of these is wired up
call.on('error', () => {});
});
afterEach(function() {
client.stop();
global.navigator = prevNavigator;
global.window = prevWindow;
global.document = prevDocument;
});
it('should ignore candidate events from non-matching party ID', async function() {
await call.placeVoiceCall();
await call.receivedAnswer({
version: 0,
call_id: call.callId,
party_id: 'the_correct_party_id',
answer: {
sdp: DUMMY_SDP,
},
});
call.peerConn.addIceCandidate = jest.fn();
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 0,
call_id: call.callId,
party_id: 'the_correct_party_id',
candidates: [
{
candidate: '',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 0,
call_id: call.callId,
party_id: 'some_other_party_id',
candidates: [
{
candidate: '',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
});
});
+86
View File
@@ -0,0 +1,86 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum EventType {
// Room state events
RoomCanonicalAlias = "m.room.canonical_alias",
RoomCreate = "m.room.create",
RoomJoinRules = "m.room.join_rules",
RoomMember = "m.room.member",
RoomThirdPartyInvite = "m.room.third_party_invite",
RoomPowerLevels = "m.room.power_levels",
RoomName = "m.room.name",
RoomTopic = "m.room.topic",
RoomAvatar = "m.room.avatar",
RoomPinnedEvents = "m.room.pinned_events",
RoomEncryption = "m.room.encryption",
RoomHistoryVisibility = "m.room.history_visibility",
RoomGuestAccess = "m.room.guest_access",
RoomServerAcl = "m.room_server_acl",
RoomTombstone = "m.room.tombstone",
/**
* @deprecated Should not be used.
*/
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
// Room timeline events
RoomRedaction = "m.room.redaction",
RoomMessage = "m.room.message",
RoomMessageEncrypted = "m.room.encrypted",
Sticker = "m.sticker",
CallInvite = "m.call.invite",
CallCandidates = "m.call.candidates",
CallAnswer = "m.call.answer",
CallHangup = "m.call.hangup",
CallReject = "m.call.reject",
KeyVerificationRequest = "m.key.verification.request",
KeyVerificationStart = "m.key.verification.start",
KeyVerificationCancel = "m.key.verification.cancel",
KeyVerificationMac = "m.key.verification.mac",
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
RoomMessageFeedback = "m.room.message.feedback",
// Room ephemeral events
Typing = "m.typing",
Receipt = "m.receipt",
Presence = "m.presence",
// Room account_data events
FullyRead = "m.fully_read",
Tag = "m.tag",
// User account_data events
PushRules = "m.push_rules",
Direct = "m.direct",
IgnoredUserList = "m.ignored_user_list",
// to_device events
RoomKey = "m.room_key",
RoomKeyRequest = "m.room_key_request",
ForwardedRoomKey = "m.forwarded_room_key",
Dummy = "m.dummy",
}
export enum MsgType {
Text = "m.text",
Emote = "m.emote",
Notice = "m.notice",
Image = "m.image",
File = "m.file",
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
}
+20
View File
@@ -25,4 +25,24 @@ declare global {
localStorage: Storage;
}
}
interface MediaDevices {
// This is experimental and types don't know about it yet
// https://github.com/microsoft/TypeScript/issues/33232
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface DummyInterfaceWeShouldntBeUsingThis {}
interface Navigator {
// We check for the webkit-prefixed getUserMedia to detect if we're
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
}
+31 -233
View File
@@ -33,6 +33,7 @@ import {EventTimeline} from "./models/event-timeline";
import {SearchResult} from "./models/search-result";
import {StubStore} from "./store/stub";
import {createNewMatrixCall} from "./webrtc/call";
import {CallEventHandler} from './webrtc/callEventHandler';
import * as utils from './utils';
import {sleep} from './utils';
import {
@@ -235,7 +236,14 @@ function keyFromRecoverySession(session, decryptionKey) {
* {
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
* "passphrase": {
* "algorithm": "m.pbkdf2",
* "iterations": 500000,
* "salt": "..."
* },
* "iv": "...",
* "mac": "..."
* }, ...
* }
* }
@@ -247,6 +255,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* desired to avoid user prompts.
* Args:
* {string} keyId the ID of the new key
* {object} keyInfo Infomation about the key as above for `getSecretStorageKey`
* {Uint8Array} key the new private key
*
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
@@ -328,17 +337,15 @@ export function MatrixClient(opts) {
}
this.clientRunning = false;
this.callList = {
// callId: MatrixCall
};
// try constructing a MatrixCall to see if we are running in an environment
// which has WebRTC. If we are, listen for and handle m.call.* events.
const call = createNewMatrixCall(this);
this._supportsVoip = false;
if (call) {
setupCallEventHandler(this);
this._callEventHandler = new CallEventHandler(this);
this._supportsVoip = true;
} else {
this._callEventHandler = null;
}
this._syncingRetry = null;
this._syncApi = null;
@@ -490,7 +497,7 @@ MatrixClient.prototype.rehydrateDevice = async function() {
},
);
} catch (e) {
logger.info("could not get dehydrated device", e);
logger.info("could not get dehydrated device", e.toString());
return;
}
@@ -1434,7 +1441,7 @@ wrapCryptoFuncs(MatrixClient, [
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
* SecretStorage#addKey: an object with `passphrase` etc fields.
*/
/**
@@ -1449,7 +1456,9 @@ wrapCryptoFuncs(MatrixClient, [
* @param {string} [keyName] the name of the key. If not given, a random
* name will be generated.
*
* @return {string} the name of the key
* @return {object} An object with:
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
/**
@@ -2009,7 +2018,7 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
*
* @param {string} password Passphrase
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<Buffer>} key backup key
* @return {Promise<Uint8Array>} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromPassword = function(
password, backupInfo,
@@ -2024,7 +2033,7 @@ MatrixClient.prototype.keyBackupKeyFromPassword = function(
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} recoveryKey The recovery key
* @return {Buffer} key backup key
* @return {Uint8Array} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
return decodeRecoveryKey(recoveryKey);
@@ -5105,6 +5114,11 @@ MatrixClient.prototype.stopClient = function() {
if (this._peekSync) {
this._peekSync.stopPeeking();
}
if (this._callEventHandler) {
this._callEventHandler.stop();
this._callEventHandler = null;
}
global.clearTimeout(this._checkTurnServersTimeoutID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
@@ -5129,7 +5143,12 @@ MatrixClient.prototype.getVersions = function() {
{
prefix: '',
},
);
).catch((e) => {
// Need to unset this if it fails, otherwise we'll never retry
this._serverVersionsPromise = null;
// but rethrow the exception to anything that was waiting
throw e;
});
return this._serverVersionsPromise;
};
@@ -5314,227 +5333,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
};
};
function setupCallEventHandler(client) {
const candidatesByCall = {
// callId: [Candidate]
};
// The sync code always emits one event at a time, so it will patiently
// wait for us to finish processing a call invite before delivering the
// next event, even if that next event is a hangup. We therefore accumulate
// all our call events and then process them on the 'sync' event, ie.
// each time a sync has completed. This way, we can avoid emitting incoming
// call events if we get both the invite and answer/hangup in the same sync.
// This happens quite often, eg. replaying sync from storage, catchup sync
// after loading and after we've been offline for a bit.
let callEventBuffer = [];
function evaluateEventBuffer() {
if (client.getSyncState() === "SYNCING") {
// don't process any events until they are all decrypted
if (callEventBuffer.some((e) => e.isBeingDecrypted())) return;
const ignoreCallIds = {}; // Set<String>
// inspect the buffer and mark all calls which have been answered
// or hung up before passing them to the call event handler.
for (let i = callEventBuffer.length - 1; i >= 0; i--) {
const ev = callEventBuffer[i];
if (ev.getType() === "m.call.answer" ||
ev.getType() === "m.call.hangup") {
ignoreCallIds[ev.getContent().call_id] = "yep";
}
}
// now loop through the buffer chronologically and inject them
callEventBuffer.forEach(function(e) {
if (
e.getType() === "m.call.invite" &&
ignoreCallIds[e.getContent().call_id]
) {
// This call has previously been answered or hung up: ignore it
return;
}
try {
callEventHandler(e);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
});
callEventBuffer = [];
}
}
client.on("sync", evaluateEventBuffer);
function onEvent(event) {
// any call events or ones that might be once they're decrypted
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
// queue up for processing once all events from this sync have been
// processed (see above).
callEventBuffer.push(event);
}
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
if (event.getType().indexOf("m.call.") === -1) return;
if (callEventBuffer.includes(event)) {
// we were waiting for that event to decrypt, so recheck the buffer
evaluateEventBuffer();
} else {
// This one wasn't buffered so just run the event handler for it
// straight away
try {
callEventHandler(event);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
}
});
}
}
client.on("event", onEvent);
function callEventHandler(event) {
const content = event.getContent();
let call = content.call_id ? client.callList[content.call_id] : undefined;
let i;
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
if (event.getType() === "m.call.invite") {
if (event.getSender() === client.credentials.userId) {
return; // ignore invites you send
}
if (event.getAge() > content.lifetime) {
return; // expired call
}
if (call && call.state === "ended") {
return; // stale/old invite event
}
if (call) {
logger.log(
"WARN: Already have a MatrixCall with id %s but got an " +
"invite. Clobbering.",
content.call_id,
);
}
call = createNewMatrixCall(client, event.getRoomId(), {
forceTURN: client._forceTURN,
});
if (!call) {
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC",
);
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
call._initWithInvite(event);
client.callList[call.callId] = call;
// if we stashed candidate events for that call ID, play them back now
if (candidatesByCall[call.callId]) {
for (i = 0; i < candidatesByCall[call.callId].length; i++) {
call._gotRemoteIceCandidate(
candidatesByCall[call.callId][i],
);
}
}
// Were we trying to call that user (room)?
let existingCall;
const existingCalls = utils.values(client.callList);
for (i = 0; i < existingCalls.length; ++i) {
const thisCall = existingCalls[i];
if (call.roomId === thisCall.roomId &&
thisCall.direction === 'outbound' &&
(["wait_local_media", "create_offer", "invite_sent"].indexOf(
thisCall.state) !== -1)) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === 'wait_local_media' ||
existingCall.state === 'create_offer' ||
existingCall.callId > call.callId) {
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
);
existingCall._replacedBy(call);
call.answer();
} else {
logger.log(
"Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId,
);
call.hangup();
}
} else {
client.emit("Call.incoming", call);
}
} else if (event.getType() === 'm.call.answer') {
if (!call) {
return;
}
if (event.getSender() === client.credentials.userId) {
if (call.state === 'ringing') {
call._onAnsweredElsewhere(content);
}
} else {
call._receivedAnswer(content);
}
} else if (event.getType() === 'm.call.candidates') {
if (event.getSender() === client.credentials.userId) {
return;
}
if (!call) {
// store the candidates; we may get a call eventually.
if (!candidatesByCall[content.call_id]) {
candidatesByCall[content.call_id] = [];
}
candidatesByCall[content.call_id] = candidatesByCall[
content.call_id
].concat(content.candidates);
} else {
for (i = 0; i < content.candidates.length; i++) {
call._gotRemoteIceCandidate(content.candidates[i]);
}
}
} else if (event.getType() === 'm.call.hangup') {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = createNewMatrixCall(client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call._initWithHangup(event);
client.callList[content.call_id] = call;
}
} else {
if (call.state !== 'ended') {
call._onHangupReceived(content);
delete client.callList[content.call_id];
}
}
}
}
}
function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
+2 -2
View File
@@ -344,14 +344,14 @@ class SSSSCryptoCallbacks {
}
}
addPrivateKey(keyId, privKey) {
addPrivateKey(keyId, keyInfo, privKey) {
this._privateKeys.set(keyId, privKey);
// Also pass along to application to cache if it wishes
if (
this._delegateCryptoCallbacks &&
this._delegateCryptoCallbacks.cacheSecretStorageKey
) {
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey);
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
}
}
}
+20 -15
View File
@@ -81,25 +81,27 @@ export class SecretStorage extends EventEmitter {
* @param {string} [keyId] the ID of the key. If not given, a random
* ID will be generated.
*
* @return {string} the ID of the key
* @return {object} An object with:
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
async addKey(algorithm, opts, keyId) {
const keyData = {algorithm};
const keyInfo = {algorithm};
if (!opts) opts = {};
if (opts.name) {
keyData.name = opts.name;
keyInfo.name = opts.name;
}
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
keyInfo.passphrase = opts.passphrase;
}
if (opts.key) {
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
keyData.iv = iv;
keyData.mac = mac;
keyInfo.iv = iv;
keyInfo.mac = mac;
}
} else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
@@ -116,10 +118,13 @@ export class SecretStorage extends EventEmitter {
}
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyData,
`m.secret_storage.key.${keyId}`, keyInfo,
);
return keyId;
return {
keyId,
keyInfo,
};
}
/**
@@ -457,13 +462,13 @@ export class SecretStorage extends EventEmitter {
if (!this._cryptoCallbacks.onSecretRequested) {
return;
}
const secret = await this._cryptoCallbacks.onSecretRequested({
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
name: content.name,
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
const secret = await this._cryptoCallbacks.onSecretRequested(
sender,
deviceId,
content.request_id,
content.name,
this._baseApis.checkDeviceTrust(sender, deviceId),
);
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
const payload = {
+6 -6
View File
@@ -633,7 +633,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
* SecretStorage#addKey: an object with `passphrase` etc fields.
*/
Crypto.prototype.bootstrapSecretStorage = async function({
createSecretStorageKey = async () => ({ }),
@@ -663,13 +663,13 @@ Crypto.prototype.bootstrapSecretStorage = async function({
opts.key = privateKey;
}
const keyId = await secretStorage.addKey(
const { keyId, keyInfo } = await secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
if (privateKey) {
// make the private key available to encrypt 4S secrets
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
}
await secretStorage.setDefaultKeyId(keyId);
@@ -682,9 +682,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
{keys: {[keyId]: keyInfo}}, "",
);
if (key) {
const keyData = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
const privateKey = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
const {iv, mac} = await SecretStorage._calculateKeyCheck(privateKey);
keyInfo.iv = iv;
keyInfo.mac = mac;
+45 -16
View File
@@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -113,7 +113,7 @@ export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac;
}
interface ICreateClientOpts {
export interface ICreateClientOpts {
baseUrl: string;
idBaseUrl?: string;
store?: Store;
@@ -126,6 +126,7 @@ interface ICreateClientOpts {
identityServer?: any;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
timelineSupport?: boolean;
queryParams?: Record<string, unknown>;
deviceToImport?: {
olmDevice: {
@@ -136,26 +137,54 @@ interface ICreateClientOpts {
userId: string;
deviceId: string;
};
pickleKey?: string;
sessionStore?: any;
unstableClientRelationAggregation?: boolean;
verificationMethods?: Array<any>;
forceTURN?: boolean;
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => unknown;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<Array<string>>;
getSecretStorageKey?: (
keys: {keys: Record<string, {pubkey: Uint8Array}>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (keyId: string, key: Uint8Array) => unknown;
onSecretRequested?: (
name: string, userId: string, deviceId: string,
requestId: string, deviceTrust: any
) => Promise<string>;
cryptoCallbacks?: ICryptoCallbacks;
}
export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<string[]>;
getSecretStorageKey?: (
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
) => void;
onSecretRequested?: (
userId: string, deviceId: string,
requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel
) => Promise<string>;
getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void,
) => Promise<Uint8Array>;
}
// TODO: Move this to `SecretStorage` once converted
export interface ISecretStorageKeyInfo {
passphrase?: {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
};
iv?: string;
mac?: string;
}
// TODO: Move this to `CrossSigning` once converted
export interface IDeviceTrustLevel {
isVerified(): boolean;
isCrossSigningVerified(): boolean;
isLocallyVerified(): boolean;
isTofu(): boolean;
}
/**
+11 -2
View File
@@ -169,6 +169,14 @@ export const MatrixEvent = function(
allows for a unique ID which does not change when the event comes back down sync.
*/
this._txnId = null;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
* it to us and the time we're now constructing this event, but that's better
* than assuming the local clock is in sync with the origin HS's clock.
*/
this._localTimestamp = Date.now() - this.getAge();
};
utils.inherits(MatrixEvent, EventEmitter);
@@ -311,11 +319,12 @@ utils.extend(MatrixEvent.prototype, {
/**
* Get the age of the event when this function was called.
* Relies on the local clock being in sync with the clock of the original homeserver.
* This is the 'age' field adjusted according to how long this client has
* had the event.
* @return {Number} The age of this event in milliseconds.
*/
getLocalAge: function() {
return Date.now() - this.getTs();
return Date.now() - this._localTimestamp;
},
/**
+2 -4
View File
@@ -358,16 +358,14 @@ Room.prototype.getPendingEvents = function() {
/**
* Check whether the pending event list contains a given event by ID.
* If pending event ordering is not "detached" then this returns false.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
return false;
}
return this._pendingEventList.some(event => event.getId() === eventId);
+2 -2
View File
@@ -226,7 +226,7 @@ LocalIndexedDBStoreBackend.prototype = {
account_data: {
events: accountData,
},
});
}, true);
});
},
@@ -416,7 +416,7 @@ LocalIndexedDBStoreBackend.prototype = {
},
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON();
const syncData = this._syncAccumulator.getJSON(true);
return Promise.all([
this._persistUserPresenceEvents(userTuples),
+47 -12
View File
@@ -87,8 +87,8 @@ export class SyncAccumulator {
};
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
accumulate(syncResponse, fromDatabase) {
this._accumulateRooms(syncResponse, fromDatabase);
this._accumulateGroups(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
@@ -107,35 +107,36 @@ export class SyncAccumulator {
/**
* Accumulate incremental /sync room data.
* @param {Object} syncResponse the complete /sync JSON
* @param {boolean} fromDatabase True if the sync response is one saved to the database
*/
_accumulateRooms(syncResponse) {
_accumulateRooms(syncResponse, fromDatabase) {
if (!syncResponse.rooms) {
return;
}
if (syncResponse.rooms.invite) {
Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
this._accumulateRoom(
roomId, "invite", syncResponse.rooms.invite[roomId],
roomId, "invite", syncResponse.rooms.invite[roomId], fromDatabase,
);
});
}
if (syncResponse.rooms.join) {
Object.keys(syncResponse.rooms.join).forEach((roomId) => {
this._accumulateRoom(
roomId, "join", syncResponse.rooms.join[roomId],
roomId, "join", syncResponse.rooms.join[roomId], fromDatabase,
);
});
}
if (syncResponse.rooms.leave) {
Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
this._accumulateRoom(
roomId, "leave", syncResponse.rooms.leave[roomId],
roomId, "leave", syncResponse.rooms.leave[roomId], fromDatabase,
);
});
}
}
_accumulateRoom(roomId, category, data) {
_accumulateRoom(roomId, category, data, fromDatabase) {
// Valid /sync state transitions
// +--------+ <======+ 1: Accept an invite
// +== | INVITE | | (5) 2: Leave a room
@@ -159,7 +160,7 @@ export class SyncAccumulator {
delete this.inviteRooms[roomId];
}
// (3)
this._accumulateJoinState(roomId, data);
this._accumulateJoinState(roomId, data, fromDatabase);
break;
case "leave":
if (this.inviteRooms[roomId]) { // (4)
@@ -203,7 +204,7 @@ export class SyncAccumulator {
}
// Accumulate timeline and state events in a room.
_accumulateJoinState(roomId, data) {
_accumulateJoinState(roomId, data, fromDatabase) {
// We expect this function to be called a lot (every /sync) so we want
// this to be fast. /sync stores events in an array but we often want
// to clobber based on type/state_key. Rather than convert arrays to
@@ -337,8 +338,20 @@ export class SyncAccumulator {
setState(currentData._currentState, e);
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
let transformedEvent;
if (!fromDatabase) {
transformedEvent = Object.assign({}, e);
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
const age = e.unsigned ? e.unsigned.age : e.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
}
currentData._timeline.push({
event: e,
event: transformedEvent,
token: index === 0 ? data.timeline.prev_batch : null,
});
});
@@ -405,6 +418,7 @@ export class SyncAccumulator {
* represents all room data that should be stored. This should be paired
* with the sync token which represents the most recent /sync response
* provided to accumulate().
* @param {boolean} forDatabase True to generate a sync to be saved to storage
* @return {Object} An object with a "nextBatch", "roomsData" and "accountData"
* keys.
* The "nextBatch" key is a string which represents at what point in the
@@ -414,7 +428,7 @@ export class SyncAccumulator {
* /sync response from the 'rooms' key onwards. The "accountData" key is
* a list of raw events which represent global account data.
*/
getJSON() {
getJSON(forDatabase) {
const data = {
join: {},
invite: {},
@@ -486,7 +500,28 @@ export class SyncAccumulator {
}
roomJson.timeline.prev_batch = msgData.token;
}
roomJson.timeline.events.push(msgData.event);
let transformedEvent;
if (!forDatabase && msgData.event._localTs) {
// This means we have to copy each event so we can fix it up to
// set a correct 'age' parameter whilst keeping the local timestamp
// on our stored event. If this turns out to be a bottleneck, it could
// be optimised either by doing this in the main process after the data
// has been structured-cloned to go between the worker & main process,
// or special-casing data from saved syncs to read the local timstamp
// directly rather than turning it into age to then immediately be
// transformed back again into a local timestamp.
transformedEvent = Object.assign({}, msgData.event);
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
delete transformedEvent._localTs;
transformedEvent.unsigned = transformedEvent.unsigned || {};
transformedEvent.unsigned.age = Date.now() - msgData.event._localTs;
} else {
transformedEvent = msgData.event;
}
roomJson.timeline.events.push(transformedEvent);
});
// Add state data: roll back current state to the start of timeline,
-1434
View File
File diff suppressed because it is too large Load Diff
+1393
View File
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixEvent from '../models/event';
import {logger} from '../logger';
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event';
import { MatrixClient } from '../client';
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button
const RING_GRACE_PERIOD = 3000;
export class CallEventHandler {
client: MatrixClient;
calls: Map<string, MatrixCall>;
callEventBuffer: MatrixEvent[];
candidateEventsByCall: Map<string, Array<MatrixEvent>>;
constructor(client: MatrixClient) {
this.client = client;
this.calls = new Map<string, MatrixCall>();
// The sync code always emits one event at a time, so it will patiently
// wait for us to finish processing a call invite before delivering the
// next event, even if that next event is a hangup. We therefore accumulate
// all our call events and then process them on the 'sync' event, ie.
// each time a sync has completed. This way, we can avoid emitting incoming
// call events if we get both the invite and answer/hangup in the same sync.
// This happens quite often, eg. replaying sync from storage, catchup sync
// after loading and after we've been offline for a bit.
this.callEventBuffer = [];
this.candidateEventsByCall = new Map<string, Array<MatrixEvent>>();
this.client.on("sync", this.evaluateEventBuffer);
this.client.on("event", this.onEvent);
}
public stop() {
this.client.removeListener("sync", this.evaluateEventBuffer);
this.client.removeListener("event", this.onEvent);
}
private evaluateEventBuffer = () => {
if (this.client.getSyncState() === "SYNCING") {
// don't process any events until they are all decrypted
if (this.callEventBuffer.some((e) => e.isBeingDecrypted())) return;
const ignoreCallIds = new Set<String>();
// inspect the buffer and mark all calls which have been answered
// or hung up before passing them to the call event handler.
for (const ev of this.callEventBuffer) {
if (ev.getType() === EventType.CallAnswer ||
ev.getType() === EventType.CallHangup) {
ignoreCallIds.add(ev.getContent().call_id);
}
}
// now loop through the buffer chronologically and inject them
for (const e of this.callEventBuffer) {
if (
e.getType() === EventType.CallInvite &&
ignoreCallIds.has(e.getContent().call_id)
) {
// This call has previously been answered or hung up: ignore it
continue;
}
try {
this.handleCallEvent(e);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
}
this.callEventBuffer = [];
}
}
private onEvent = (event: MatrixEvent) => {
// any call events or ones that might be once they're decrypted
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
// queue up for processing once all events from this sync have been
// processed (see above).
this.callEventBuffer.push(event);
}
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
if (event.getType().indexOf("m.call.") === -1) return;
if (this.callEventBuffer.includes(event)) {
// we were waiting for that event to decrypt, so recheck the buffer
this.evaluateEventBuffer();
} else {
// This one wasn't buffered so just run the event handler for it
// straight away
try {
this.handleCallEvent(event);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
}
});
}
}
private handleCallEvent(event: MatrixEvent) {
const content = event.getContent();
let call = content.call_id ? this.calls.get(content.call_id) : undefined;
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
if (event.getType() === EventType.CallInvite) {
if (event.getSender() === this.client.credentials.userId) {
return; // ignore invites you send
}
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) {
return; // expired call
}
if (call && call.state === CallState.Ended) {
return; // stale/old invite event
}
if (call) {
logger.log(
`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
`invite. Clobbering.`,
);
}
call = createNewMatrixCall(this.client, event.getRoomId(), {
forceTURN: this.client._forceTURN,
});
if (!call) {
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC",
);
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
call.initWithInvite(event);
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) {
call.onRemoteIceCandidatesReceived(ev);
}
}
// Were we trying to call that user (room)?
let existingCall;
for (const thisCall of this.calls.values()) {
const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
thisCall.state,
);
if (
call.roomId === thisCall.roomId &&
thisCall.direction === CallDirection.Outbound &&
isCalling
) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === CallState.WaitLocalMedia ||
existingCall.state === CallState.CreateOffer ||
existingCall.callId > call.callId) {
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
);
existingCall.replacedBy(call);
call.answer();
} else {
logger.log(
"Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId,
);
call.hangup(CallErrorCode.Replaced, true);
}
} else {
this.client.emit("Call.incoming", call);
}
} else if (event.getType() === EventType.CallAnswer) {
if (!call) {
return;
}
if (event.getSender() === this.client.credentials.userId) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
} else {
call.receivedAnswer(content);
}
} else if (event.getType() === EventType.CallCandidates) {
if (event.getSender() === this.client.credentials.userId) {
return;
}
if (!call) {
// store the candidates; we may get a call eventually.
if (!this.candidateEventsByCall.has(content.call_id)) {
this.candidateEventsByCall.set(content.call_id, []);
}
this.candidateEventsByCall.get(content.call_id).push(event);
} else {
call.onRemoteIceCandidatesReceived(event);
}
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType())) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = createNewMatrixCall(this.client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call.initWithHangup(event);
this.calls.set(content.call_id, call);
}
} else {
if (call.state !== CallState.Ended) {
if (event.getType() === EventType.CallHangup) {
call.onHangupReceived(content);
} else {
call.onRejectReceived(content);
}
this.calls.delete(content.call_id);
}
}
}
}
}
+71 -1
View File
@@ -1295,6 +1295,16 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"
"@jest/types@^25.5.0":
version "25.5.0"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d"
integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==
dependencies:
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@types/babel-types@*", "@types/babel-types@^7.0.0":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.9.tgz#01d7b86949f455402a94c788883fe4ba574cad41"
@@ -1375,6 +1385,14 @@
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-lib-report" "*"
"@types/jest@^26.0.14":
version "26.0.14"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.14.tgz#078695f8f65cb55c5a98450d65083b2b73e5a3f3"
integrity sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg==
dependencies:
jest-diff "^25.2.1"
pretty-format "^25.2.1"
"@types/json-schema@^7.0.3":
version "7.0.4"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
@@ -1427,6 +1445,13 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yargs@^15.0.0":
version "15.0.8"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.8.tgz#7644904cad7427eb704331ea9bf1ee5499b82e23"
integrity sha512-b0BYzFUzBpOhPjpl1wtAHU994jBeKF4TKVlT7ssFv44T617XNcPdRoG4AzHLVshLzlrF7i3lTelH7UbuNYV58Q==
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^3.4.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.0.tgz#0f91aa3c83d019591719e597fbdb73a59595a263"
@@ -1657,6 +1682,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@@ -2298,6 +2330,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
@@ -2767,6 +2807,11 @@ diff-sequences@^24.9.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
diff-sequences@^25.2.6:
version "25.2.6"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -4340,6 +4385,16 @@ jest-diff@^24.9.0:
jest-get-type "^24.9.0"
pretty-format "^24.9.0"
jest-diff@^25.2.1:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9"
integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==
dependencies:
chalk "^3.0.0"
diff-sequences "^25.2.6"
jest-get-type "^25.2.6"
pretty-format "^25.5.0"
jest-docblock@^24.3.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
@@ -4386,6 +4441,11 @@ jest-get-type@^24.9.0:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
jest-get-type@^25.2.6:
version "25.2.6"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877"
integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==
jest-haste-map@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
@@ -5717,6 +5777,16 @@ pretty-format@^24.9.0:
ansi-styles "^3.2.0"
react-is "^16.8.4"
pretty-format@^25.2.1, pretty-format@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==
dependencies:
"@jest/types" "^25.5.0"
ansi-regex "^5.0.0"
ansi-styles "^4.0.0"
react-is "^16.12.0"
private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -5977,7 +6047,7 @@ react-frame-component@^4.1.1:
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.1.3.tgz#64c09dd29574720879c5f43ee36c17d8ae74d4ec"
integrity sha512-4PurhctiqnmC1F5prPZ+LdsalH7pZ3SFA5xoc0HBe8mSHctdLLt4Cr2WXfXOoajHBYq/yiipp9zOgx+vy8GiEA==
react-is@^16.8.1, react-is@^16.8.4:
react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==