Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| a449c5f8c2 | |||
| 57d1fa8410 | |||
| 838e38d84c | |||
| fc29056530 | |||
| 4e967c979c | |||
| 62a34848c7 | |||
| 01fe6cc542 | |||
| 3fdc25777d | |||
| 3b7d6f8334 | |||
| 4b3c8b2969 | |||
| ad80d69369 | |||
| e2d2249686 | |||
| dc64c34ccb | |||
| 0bf50659ab | |||
| ec26b16ddf | |||
| a044b74a1d | |||
| 1e7a1dce90 | |||
| 41c2772cff | |||
| 1bba2bc0ed | |||
| e11c523a75 | |||
| b911a890cf | |||
| c8f69c0b79 | |||
| 8a6248f120 | |||
| 340fa6c63e | |||
| 1177bf39a2 | |||
| 88b310c394 | |||
| 973de2db55 | |||
| 1fe92f10c1 | |||
| 0e2e906b24 | |||
| 4667f8be03 | |||
| 4a51ac7a74 | |||
| 6099efe41a | |||
| 4254d595fc | |||
| 069ca4a89d | |||
| e3ba08fbbc | |||
| dd84e51161 | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| b435b582bd | |||
| d46021a05e | |||
| 1b31d0622e | |||
| 68206a6e19 | |||
| 56797948af | |||
| c0af2f25a1 | |||
| dc12b1df00 | |||
| b13f5aebfd | |||
| 338301bb5d | |||
| 72a0931663 | |||
| 67584b9cc3 | |||
| 1bfaa28f9c | |||
| cbe9b59222 | |||
| 276b52f0fe | |||
| 07f49bcc37 | |||
| 09fac77ce0 | |||
| 102704e91a | |||
| c5fb351baa | |||
| 98f8d4414d | |||
| 1dddcd4925 | |||
| 2666a271a5 | |||
| e277de6e3d | |||
| daa17b3287 | |||
| c7f887131e | |||
| 58546b80d0 | |||
| 466f749b71 | |||
| 4c2a83c470 | |||
| d534ab18c7 | |||
| 64aaed833b | |||
| 2f05be599c | |||
| 7371f8dd3a | |||
| 79aefe9707 | |||
| 6bc80577ee | |||
| 837764190f | |||
| 61948d70e3 | |||
| 9fb0385694 | |||
| 193de8d3a9 | |||
| 1f2b3512c1 | |||
| ddc5bd3b36 | |||
| ee828d454a | |||
| 2916a73f4c | |||
| ae69af7e70 | |||
| 1c6459fe65 | |||
| adaeb42416 | |||
| f1e1daa194 | |||
| 401e89ef78 | |||
| 59e0bd467c | |||
| 7f4397f8ca | |||
| 32830b93f1 | |||
| cdc0d5623b | |||
| e78b415832 | |||
| ff1379fd29 | |||
| 3820c15ecf | |||
| 26ef33e4f3 | |||
| 0534a4ed1b | |||
| f29a24a915 | |||
| cecbcd941e | |||
| 6be99d6397 | |||
| 4e5947af51 | |||
| 4204b2170a | |||
| 0a5ad489b6 | |||
| 02b283be78 | |||
| 10c49c0fd1 | |||
| 9ecc0f5d95 | |||
| 972b59b99e | |||
| 34bb05bd88 | |||
| 37fb21f726 | |||
| b42efa4a07 | |||
| 20b20738a7 | |||
| b28a191c4e | |||
| f92b620434 | |||
| bf25cb68da | |||
| 0b063f6b8b | |||
| d8134aa168 | |||
| 702b3e8473 | |||
| 6381018658 | |||
| 1c191b2278 | |||
| a489691151 | |||
| 0bfcb5071d | |||
| ceb162eb01 | |||
| b307a177f4 | |||
| 059430bd0a | |||
| 530b60cbc2 | |||
| ba8577f268 |
+3
-4
@@ -1,13 +1,12 @@
|
||||
module.exports = {
|
||||
extends: ["matrix-org"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"matrix-org",
|
||||
],
|
||||
extends: ["plugin:matrix-org/javascript"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
@@ -33,7 +32,7 @@ module.exports = {
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"extends": ["plugin:matrix-org/typescript"],
|
||||
"rules": {
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
|
||||
+114
@@ -1,3 +1,117 @@
|
||||
Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0)
|
||||
|
||||
* [Release] Fix regressed glare
|
||||
[\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695)
|
||||
|
||||
Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Switch from MSC1772 unstable prefixes to stable
|
||||
[\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679)
|
||||
* Update the VoIP example to work with the new changes
|
||||
[\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680)
|
||||
* Bump hosted-git-info from 2.8.8 to 2.8.9
|
||||
[\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687)
|
||||
* Support for multiple streams (not MSC3077)
|
||||
[\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660)
|
||||
* Tweak missing m.room.create errors to describe their source
|
||||
[\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683)
|
||||
|
||||
Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0-rc.1) (2021-05-04)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0...v10.1.0-rc.1)
|
||||
|
||||
* Revert "Raise logging dramatically to chase pending event errors"
|
||||
[\#1681](https://github.com/matrix-org/matrix-js-sdk/pull/1681)
|
||||
* Add test coverage collection script
|
||||
[\#1677](https://github.com/matrix-org/matrix-js-sdk/pull/1677)
|
||||
* Raise logging dramatically to chase pending event errors
|
||||
[\#1678](https://github.com/matrix-org/matrix-js-sdk/pull/1678)
|
||||
* Support MSC3086 asserted identity
|
||||
[\#1674](https://github.com/matrix-org/matrix-js-sdk/pull/1674)
|
||||
* Fix `/search` with no results field work again
|
||||
[\#1670](https://github.com/matrix-org/matrix-js-sdk/pull/1670)
|
||||
* Add room.getMembers method
|
||||
[\#1672](https://github.com/matrix-org/matrix-js-sdk/pull/1672)
|
||||
|
||||
Changes in [10.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0) (2021-04-26)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0-rc.1...v10.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0-rc.1) (2021-04-21)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0...v10.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* The `RoomState.members` event is now only emitted when the room member's power level or the room's normal power level actually changes
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Restrict event emit for room members that had power levels changed
|
||||
[\#1675](https://github.com/matrix-org/matrix-js-sdk/pull/1675)
|
||||
* Fix sync with misconfigured push rules
|
||||
[\#1669](https://github.com/matrix-org/matrix-js-sdk/pull/1669)
|
||||
* Add missing await
|
||||
[\#1665](https://github.com/matrix-org/matrix-js-sdk/pull/1665)
|
||||
* Migrate to `eslint-plugin-matrix-org`
|
||||
[\#1642](https://github.com/matrix-org/matrix-js-sdk/pull/1642)
|
||||
* Add missing event type enum for key verification done
|
||||
[\#1664](https://github.com/matrix-org/matrix-js-sdk/pull/1664)
|
||||
* Fix timeline jumpiness by setting correct txnId
|
||||
[\#1663](https://github.com/matrix-org/matrix-js-sdk/pull/1663)
|
||||
* Fix calling addEventListener if it does not exist
|
||||
[\#1661](https://github.com/matrix-org/matrix-js-sdk/pull/1661)
|
||||
* Persist unsent messages for subsequent sessions
|
||||
[\#1655](https://github.com/matrix-org/matrix-js-sdk/pull/1655)
|
||||
|
||||
Changes in [9.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0) (2021-04-12)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0-rc.1...v9.11.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0-rc.1) (2021-04-07)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0...v9.11.0-rc.1)
|
||||
|
||||
* Only try to cache private keys we know exist
|
||||
[\#1657](https://github.com/matrix-org/matrix-js-sdk/pull/1657)
|
||||
* Properly terminate screen-share calls if NoUserMedia
|
||||
[\#1654](https://github.com/matrix-org/matrix-js-sdk/pull/1654)
|
||||
* Attended transfer
|
||||
[\#1652](https://github.com/matrix-org/matrix-js-sdk/pull/1652)
|
||||
* Remove catch handlers in private key retrieval
|
||||
[\#1653](https://github.com/matrix-org/matrix-js-sdk/pull/1653)
|
||||
* Fixed the media fail error on caller's side
|
||||
[\#1651](https://github.com/matrix-org/matrix-js-sdk/pull/1651)
|
||||
* Add function to share megolm keys for historical messages, take 2
|
||||
[\#1640](https://github.com/matrix-org/matrix-js-sdk/pull/1640)
|
||||
* Cache cross-signing private keys if needed on bootstrap
|
||||
[\#1649](https://github.com/matrix-org/matrix-js-sdk/pull/1649)
|
||||
|
||||
Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0)
|
||||
|
||||
@@ -31,6 +31,23 @@ function addListeners(call) {
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
const remoteElement = document.getElementById("remote");
|
||||
const localElement = document.getElementById("local");
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteElement.srcObject = remoteFeed.stream;
|
||||
remoteElement.play();
|
||||
}
|
||||
if (localFeed) {
|
||||
localElement.muted = true;
|
||||
localElement.srcObject = localFeed.stream;
|
||||
localElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@@ -62,10 +79,7 @@ function syncComplete() {
|
||||
);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall(
|
||||
document.getElementById("remote"),
|
||||
document.getElementById("local")
|
||||
);
|
||||
call.placeVideoCall();
|
||||
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
+21
-13
@@ -1,26 +1,34 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="remote"></video>
|
||||
</div>
|
||||
</div>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
height: 500px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+20
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.10.0",
|
||||
"version": "11.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -17,8 +17,9 @@
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 72 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -61,6 +62,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
@@ -72,15 +75,16 @@
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-matrix-org": "^0.2.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
@@ -94,7 +98,16 @@
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -193,7 +193,9 @@ describe("Cross Signing", function() {
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust();
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
@@ -49,9 +49,9 @@ export async function makeTestClients(userInfos, options) {
|
||||
};
|
||||
const sendEvent = function(room, type, content) {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
@@ -61,13 +61,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
|
||||
},
|
||||
}));
|
||||
|
||||
setImmediate(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
} else {
|
||||
|
||||
@@ -46,8 +46,8 @@ describe("realtime-callbacks", function() {
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this).toBe(global); // eslint-disable-line @babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @babel/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {EventStatus, MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {Room} from "../../src/models/room";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -1176,7 +1177,10 @@ describe("Room", function() {
|
||||
describe("addPendingEvent", function() {
|
||||
it("should add pending events to the pendingEventList if " +
|
||||
"pendingEventOrdering == 'detached'", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1226,7 +1230,10 @@ describe("Room", function() {
|
||||
|
||||
describe("updatePendingEvent", function() {
|
||||
it("should remove cancelled events from the pending list", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {MatrixCall, CallErrorCode} from '../../../src/webrtc/call';
|
||||
import {MatrixCall, CallErrorCode, CallEvent} from '../../../src/webrtc/call';
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
@@ -254,4 +254,48 @@ describe('Call', function() {
|
||||
sdpMid: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const identChangedCallback = jest.fn();
|
||||
call.on(CallEvent.AssertedIdentityChanged, identChangedCallback);
|
||||
|
||||
await call.onAssertedIdentityReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
asserted_identity: {
|
||||
id: "@steve:example.com",
|
||||
display_name: "Steve Gibbons",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(identChangedCallback).toHaveBeenCalled();
|
||||
|
||||
const ident = call.getRemoteAssertedIdentity();
|
||||
expect(ident.id).toEqual("@steve:example.com");
|
||||
expect(ident.displayName).toEqual("Steve Gibbons");
|
||||
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
});
|
||||
|
||||
+7
-5
@@ -36,9 +36,8 @@ export enum EventType {
|
||||
*/
|
||||
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
|
||||
// Spaces MSC1772
|
||||
SpaceChild = "org.matrix.msc1772.space.child",
|
||||
SpaceParent = "org.matrix.msc1772.space.parent",
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
// Room timeline events
|
||||
RoomRedaction = "m.room.redaction",
|
||||
@@ -53,10 +52,13 @@ export enum EventType {
|
||||
CallSelectAnswer = "m.call.select_answer",
|
||||
CallNegotiate = "m.call.negotiate",
|
||||
CallReplaces = "m.call.replaces",
|
||||
CallAssertedIdentity = "m.call.asserted_identity",
|
||||
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
KeyVerificationDone = "m.key.verification.done",
|
||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||
RoomMessageFeedback = "m.room.message.feedback",
|
||||
|
||||
@@ -92,8 +94,8 @@ export enum MsgType {
|
||||
Video = "m.video",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
|
||||
export const RoomCreateTypeField = "type";
|
||||
|
||||
export enum RoomType {
|
||||
Space = "org.matrix.msc1772.space", // Spaces MSC1772
|
||||
Space = "m.space",
|
||||
}
|
||||
|
||||
+47
-2
@@ -500,7 +500,7 @@ MatrixClient.prototype.rehydrateDevice = async function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const getDeviceResult = this.getDehydratedDevice();
|
||||
const getDeviceResult = await this.getDehydratedDevice();
|
||||
if (!getDeviceResult) {
|
||||
return;
|
||||
}
|
||||
@@ -729,6 +729,17 @@ MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer)
|
||||
this._supportsCallTransfer = supportsCallTransfer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new call.
|
||||
* The place*Call methods on the returned call can be used to actually place a call
|
||||
*
|
||||
* @param {string} roomId The room the call is to be placed in.
|
||||
* @return {MatrixCall} the call or null if the browser doesn't support calling.
|
||||
*/
|
||||
MatrixClient.prototype.createCall = function(roomId) {
|
||||
return createNewMatrixCall(this, roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current sync state.
|
||||
* @return {?string} the sync state, which may be null.
|
||||
@@ -2303,6 +2314,39 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Share shared-history decryption keys with the given users.
|
||||
*
|
||||
* @param {string} roomId the room for which keys should be shared.
|
||||
* @param {array} userIds a list of users to share with. The keys will be sent to
|
||||
* all of the user's current devices.
|
||||
*/
|
||||
MatrixClient.prototype.sendSharedHistoryKeys = async function(roomId, userIds) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const roomEncryption = this._roomList.getRoomEncryption(roomId);
|
||||
if (!roomEncryption) {
|
||||
// unknown room, or unencrypted room
|
||||
logger.error("Unknown room. Not sharing decryption keys");
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceInfos = await this._crypto.downloadKeys(userIds);
|
||||
const devicesByUser = {};
|
||||
for (const [userId, devices] of Object.entries(deviceInfos)) {
|
||||
devicesByUser[userId] = Object.values(devices);
|
||||
}
|
||||
|
||||
const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm);
|
||||
if (alg.sendSharedHistoryInboundSessions) {
|
||||
await alg.sendSharedHistoryInboundSessions(devicesByUser);
|
||||
} else {
|
||||
logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
|
||||
}
|
||||
};
|
||||
|
||||
// Group ops
|
||||
// =========
|
||||
// Operations on groups that come down the sync stream (ie. ones the
|
||||
@@ -4766,7 +4810,8 @@ MatrixClient.prototype._processRoomEventsSearch = function(searchResults, respon
|
||||
searchResults.highlights = Object.keys(highlights);
|
||||
|
||||
// append the new results to our existing results
|
||||
for (let i = 0; i < room_events.results.length; i++) {
|
||||
const resultsLength = room_events.results ? room_events.results.length : 0;
|
||||
for (let i = 0; i < resultsLength; i++) {
|
||||
const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
|
||||
searchResults.results.push(sr);
|
||||
}
|
||||
|
||||
@@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this._getInboundGroupSession(
|
||||
@@ -1104,6 +1105,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this._cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
@@ -1383,6 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
"shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1415,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
|
||||
"session_key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||
"first_known_index": session.first_known_index(),
|
||||
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
},
|
||||
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Utilities
|
||||
// =========
|
||||
|
||||
|
||||
+147
-31
@@ -36,11 +36,27 @@ import {
|
||||
|
||||
import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
function isRoomSharedHistory(room) {
|
||||
const visibilityEvent = room.currentState &&
|
||||
room.currentState.getStateEvents("m.room.history_visibility", "");
|
||||
// NOTE: if the room visibility is unset, it would normally default to
|
||||
// "world_readable".
|
||||
// (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
|
||||
// But we will be paranoid here, and treat it as a situation where the room
|
||||
// is not shared-history
|
||||
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
||||
visibilityEvent.getContent().history_visibility;
|
||||
return ["world_readable", "shared"].includes(visibility);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} sessionId
|
||||
* @param {boolean} sharedHistory whether the session can be freely shared with
|
||||
* other group members, according to the room history visibility settings
|
||||
*
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
@@ -50,12 +66,13 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
function OutboundSessionInfo(sessionId, sharedHistory = false) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
this.blockedDevicesNotified = {};
|
||||
this.sharedHistory = sharedHistory;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +200,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
||||
* @param {Object} blocked The devices that are blocked, indexed by user ID
|
||||
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
|
||||
@@ -192,7 +210,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
room, devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
) {
|
||||
let session;
|
||||
|
||||
@@ -204,6 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const prepareSession = async (oldSession) => {
|
||||
session = oldSession;
|
||||
|
||||
const sharedHistory = isRoomSharedHistory(room);
|
||||
|
||||
// history visibility changed
|
||||
if (session && sharedHistory !== session.sharedHistory) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
|
||||
this._sessionRotationPeriodMs)
|
||||
@@ -219,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
|
||||
if (!session) {
|
||||
logger.log(`Starting new megolm session for room ${this._roomId}`);
|
||||
session = await this._prepareNewSession();
|
||||
session = await this._prepareNewSession(sharedHistory);
|
||||
logger.log(`Started new megolm session ${session.sessionId} ` +
|
||||
`for room ${this._roomId}`);
|
||||
this._outboundSessions[session.sessionId] = session;
|
||||
@@ -250,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session.sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": session.sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"org.matrix.msc3061.shared_history": sharedHistory,
|
||||
},
|
||||
};
|
||||
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
|
||||
@@ -374,15 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {boolean} sharedHistory
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
||||
const sessionId = this._olmDevice.createOutboundGroupSession();
|
||||
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||
|
||||
await this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false,
|
||||
{sharedHistory: sharedHistory},
|
||||
);
|
||||
|
||||
// don't wait for it to complete
|
||||
@@ -391,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
sessionId, key.key,
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(sessionId);
|
||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -672,14 +701,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
const payload = {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -901,7 +931,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
|
||||
}
|
||||
|
||||
logger.debug(`Ensuring outbound session in ${this._roomId}`);
|
||||
await this._ensureOutboundSession(devicesInRoom, blocked, true);
|
||||
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
|
||||
|
||||
logger.debug(`Ready to encrypt events for ${this._roomId}`);
|
||||
} catch (e) {
|
||||
@@ -945,7 +975,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
|
||||
this._checkForUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
|
||||
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
|
||||
const payloadJson = {
|
||||
room_id: this._roomId,
|
||||
type: eventType,
|
||||
@@ -1370,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
const extraSessionData = {};
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData,
|
||||
).then(() => {
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId)
|
||||
@@ -1573,14 +1607,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
return {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1594,6 +1629,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @param {string} [opts.source] where the key came from
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
const extraSessionData = {};
|
||||
if (opts.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
if (session["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
@@ -1602,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
extraSessionData,
|
||||
).then(() => {
|
||||
if (opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
@@ -1681,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
|
||||
return !this._pendingEvents[senderKey];
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser,
|
||||
);
|
||||
|
||||
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
|
||||
|
||||
const sharedHistorySessions =
|
||||
await this._olmDevice.getSharedHistoryInboundGroupSessions(
|
||||
this._roomId,
|
||||
);
|
||||
logger.log("shared-history sessions", sharedHistorySessions);
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this._buildKeyForwardingMessage(
|
||||
this._roomId, senderKey, sessionId,
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
const contentMap = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
contentMap[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
|
||||
promises.push(
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._userId,
|
||||
this._deviceId,
|
||||
this._olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// prune out any devices that encryptMessageForDevice could not encrypt for,
|
||||
// in which case it will have just not added anything to the ciphertext object.
|
||||
// There's no point sending messages to devices if we couldn't encrypt to them,
|
||||
// since that's effectively a blank message.
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
|
||||
logger.log(
|
||||
"No ciphertext for device " +
|
||||
userId + ":" + deviceId + ": pruning",
|
||||
);
|
||||
delete contentMap[userId][deviceId];
|
||||
}
|
||||
}
|
||||
// No devices left for that user? Strip that too.
|
||||
if (Object.keys(contentMap[userId]).length === 0) {
|
||||
logger.log("Pruned all devices for user " + userId);
|
||||
delete contentMap[userId];
|
||||
}
|
||||
}
|
||||
|
||||
// Is there anything left?
|
||||
if (Object.keys(contentMap).length === 0) {
|
||||
logger.log("No users left to send to: aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
await this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}
|
||||
};
|
||||
|
||||
registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
|
||||
);
|
||||
|
||||
+39
-8
@@ -568,7 +568,9 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
||||
"Cross-signing private keys not found locally, but they are available " +
|
||||
"in secret storage, reading storage and caching locally",
|
||||
);
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
await this.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming no app-supplied callback, default to storing new private keys in
|
||||
@@ -1300,13 +1302,19 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
*/
|
||||
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
Crypto.prototype.checkOwnCrossSigningTrust = async function({
|
||||
allowPrivateKeyRequests = false,
|
||||
} = {}) {
|
||||
const userId = this._userId;
|
||||
|
||||
// Before proceeding, ensure our cross-signing public keys have been
|
||||
// downloaded via the device list.
|
||||
await this.downloadKeys([this._userId]);
|
||||
|
||||
// Also check which private keys are locally cached.
|
||||
const crossSigningPrivateKeys =
|
||||
await this._crossSigningInfo.getCrossSigningKeysFromCache();
|
||||
|
||||
// If we see an update to our own master key, check it against the master
|
||||
// key we have and, if it matches, mark it as verified
|
||||
|
||||
@@ -1322,18 +1330,26 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
|
||||
const seenPubkey = newCrossSigning.getId();
|
||||
const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
|
||||
const masterExistsNotLocallyCached =
|
||||
newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
|
||||
if (masterChanged) {
|
||||
logger.info("Got new master public key", seenPubkey);
|
||||
}
|
||||
if (
|
||||
allowPrivateKeyRequests &&
|
||||
(masterChanged || masterExistsNotLocallyCached)
|
||||
) {
|
||||
logger.info("Attempting to retrieve cross-signing master private key");
|
||||
let signing = null;
|
||||
// It's important for control flow that we leave any errors alone for
|
||||
// higher levels to handle so that e.g. cancelling access properly
|
||||
// aborts any larger operation as well.
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
'master', seenPubkey,
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info("Got cross-signing master private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing master private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
@@ -1348,10 +1364,24 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing");
|
||||
const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing");
|
||||
|
||||
const selfSigningExistsNotLocallyCached = (
|
||||
newCrossSigning.getId("self_signing") &&
|
||||
!crossSigningPrivateKeys.has("self_signing")
|
||||
);
|
||||
const userSigningExistsNotLocallyCached = (
|
||||
newCrossSigning.getId("user_signing") &&
|
||||
!crossSigningPrivateKeys.has("user_signing")
|
||||
);
|
||||
|
||||
const keySignatures = {};
|
||||
|
||||
if (selfSigningChanged) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
}
|
||||
if (
|
||||
allowPrivateKeyRequests &&
|
||||
(selfSigningChanged || selfSigningExistsNotLocallyCached)
|
||||
) {
|
||||
logger.info("Attempting to retrieve cross-signing self-signing private key");
|
||||
let signing = null;
|
||||
try {
|
||||
@@ -1360,8 +1390,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info("Got cross-signing self-signing private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing self-signing private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
@@ -1374,6 +1402,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
}
|
||||
if (userSigningChanged) {
|
||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
||||
}
|
||||
if (
|
||||
allowPrivateKeyRequests &&
|
||||
(userSigningChanged || userSigningExistsNotLocallyCached)
|
||||
) {
|
||||
logger.info("Attempting to retrieve cross-signing user-signing private key");
|
||||
let signing = null;
|
||||
try {
|
||||
@@ -1382,8 +1415,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info("Got cross-signing user-signing private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing user-signing private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
|
||||
export const VERSION = 9;
|
||||
export const VERSION = 10;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
@@ -759,6 +759,38 @@ export class Backend {
|
||||
}));
|
||||
}
|
||||
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readwrite",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
const {sessions} = req.result || {sessions: []};
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({roomId, sessions});
|
||||
};
|
||||
}
|
||||
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readonly",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => {
|
||||
const {sessions} = req.result || {sessions: []};
|
||||
resolve(sessions);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func, log = logger) {
|
||||
let startTime;
|
||||
let description;
|
||||
@@ -834,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
|
||||
return this._backend.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {string} senderKey The sender's curve 25519 key
|
||||
* @param {string} sessionId The ID of the session
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
*/
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
this._backend.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
|
||||
*/
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
@@ -614,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||
= 'inbound_group_sessions_withheld';
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
|
||||
= 'shared_history_inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
@@ -51,6 +51,8 @@ export class MemoryCryptoStore {
|
||||
this._rooms = {};
|
||||
// Set of {senderCurve25519Key+'/'+sessionId}
|
||||
this._sessionsNeedingBackup = {};
|
||||
// roomId -> array of [senderKey, sessionId]
|
||||
this._sharedHistoryInboundGroupSessions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,6 +469,16 @@ export class MemoryCryptoStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
|
||||
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || [];
|
||||
sessions.push([senderKey, sessionId]);
|
||||
this._sharedHistoryInboundGroupSessions[roomId] = sessions;
|
||||
}
|
||||
|
||||
getSharedHistoryInboundGroupSessions(roomId) {
|
||||
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []);
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
|
||||
+2
-2
@@ -36,11 +36,11 @@ const DEFAULT_NAMESPACE = "matrix";
|
||||
// when logging so we always get the current value of console methods.
|
||||
log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
return function(...args) {
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
if (this.prefix) {
|
||||
args.unshift(this.prefix);
|
||||
}
|
||||
/* eslint-enable babel/no-invalid-this */
|
||||
/* eslint-enable @babel/no-invalid-this */
|
||||
const supportedByConsole = methodName === "error" ||
|
||||
methodName === "warn" ||
|
||||
methodName === "trace" ||
|
||||
|
||||
@@ -55,7 +55,6 @@ export * from "./content-repo";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
setAudioOutput as setMatrixCallAudioOutput,
|
||||
setAudioInput as setMatrixCallAudioInput,
|
||||
setVideoInput as setMatrixCallVideoInput,
|
||||
} from "./webrtc/call";
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ export const MatrixEvent = function(
|
||||
/* The txnId with which this event was sent if it was during this session,
|
||||
allows for a unique ID which does not change when the event comes back down sync.
|
||||
*/
|
||||
this._txnId = null;
|
||||
this._txnId = event.txn_id || null;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
|
||||
@@ -351,8 +351,14 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
} else if (event.getType() === "m.room.power_levels") {
|
||||
const members = utils.values(self.members);
|
||||
utils.forEach(members, function(member) {
|
||||
// We only propagate `RoomState.members` event if the
|
||||
// power levels has been changed
|
||||
// large room suffer from large re-rendering especially when not needed
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
}
|
||||
});
|
||||
|
||||
// assume all our sentinels are now out-of-date
|
||||
|
||||
+102
-11
@@ -123,6 +123,8 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
opts = opts || {};
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
|
||||
this._client = client;
|
||||
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
@@ -189,6 +191,18 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList = [];
|
||||
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId));
|
||||
if (serializedPendingEventList) {
|
||||
JSON.parse(serializedPendingEventList)
|
||||
.forEach(async serializedEvent => {
|
||||
const event = new MatrixEvent(serializedEvent);
|
||||
if (event.getType() === "m.room.encrypted") {
|
||||
await event.attemptDecryption(this._client._crypto);
|
||||
}
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
this.addPendingEvent(event, event.getTxnId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// read by megolm; boolean value - null indicates "use global value"
|
||||
@@ -197,7 +211,6 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
this._summaryHeroes = null;
|
||||
// awaited by getEncryptionTargetMembers while room members are loading
|
||||
|
||||
this._client = client;
|
||||
if (!this._opts.lazyLoadMembers) {
|
||||
this._membersPromise = Promise.resolve();
|
||||
} else {
|
||||
@@ -205,6 +218,14 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomId ID of the current room
|
||||
* @returns {string} Storage key to retrieve pending events
|
||||
*/
|
||||
function pendingEventsKey(roomId) {
|
||||
return `mx_pending_events_${roomId}`;
|
||||
}
|
||||
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
/**
|
||||
@@ -214,7 +235,7 @@ utils.inherits(Room, EventEmitter);
|
||||
Room.prototype.getVersion = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) {
|
||||
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
return '1';
|
||||
}
|
||||
const ver = createEvent.getContent()['room_version'];
|
||||
@@ -357,6 +378,31 @@ Room.prototype.getPendingEvents = function() {
|
||||
return this._pendingEventList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a pending event for this room
|
||||
*
|
||||
* @param {string} eventId
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
Room.prototype.removePendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
throw new Error(
|
||||
"Cannot call removePendingEvent with pendingEventOrdering == " +
|
||||
this._opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
const removed = utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) {
|
||||
return ev.getId() == eventId;
|
||||
}, false,
|
||||
);
|
||||
|
||||
this._savePendingEvents();
|
||||
|
||||
return removed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the pending event list contains a given event by ID.
|
||||
* If pending event ordering is not "detached" then this returns false.
|
||||
@@ -936,6 +982,15 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
return this.currentState.getMember(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all currently loaded members from the current
|
||||
* room state.
|
||||
* @returns {RoomMember[]} Room members
|
||||
*/
|
||||
Room.prototype.getMembers = function() {
|
||||
return this.currentState.getMembers();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of members whose membership state is "join".
|
||||
* @return {RoomMember[]} A list of currently joined members.
|
||||
@@ -1192,7 +1247,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
||||
* unique transaction id.
|
||||
*/
|
||||
Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
if (event.status !== EventStatus.SENDING) {
|
||||
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
|
||||
throw new Error("addPendingEvent called on an event with status " +
|
||||
event.status);
|
||||
}
|
||||
@@ -1219,7 +1274,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
}
|
||||
this._pendingEventList.push(event);
|
||||
|
||||
this._savePendingEvents();
|
||||
if (event.isRelation()) {
|
||||
// For pending events, add them to the relations collection immediately.
|
||||
// (The alternate case below already covers this as part of adding to
|
||||
@@ -1256,6 +1311,46 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists all pending events to local storage
|
||||
*
|
||||
* If the current room is encrypted only encrypted events will be persisted
|
||||
* all messages that are not yet encrypted will be discarded
|
||||
*
|
||||
* This is because the flow of EVENT_STATUS transition is
|
||||
* queued => sending => encrypting => sending => sent
|
||||
*
|
||||
* Steps 3 and 4 are skipped for unencrypted room.
|
||||
* It is better to discard an unencrypted message rather than persisting
|
||||
* it locally for everyone to read
|
||||
*/
|
||||
Room.prototype._savePendingEvents = function() {
|
||||
if (this._pendingEventList) {
|
||||
const pendingEvents = this._pendingEventList.map(event => {
|
||||
return {
|
||||
...event.event,
|
||||
txn_id: event.getTxnId(),
|
||||
};
|
||||
}).filter(event => {
|
||||
// Filter out the unencrypted messages if the room is encrypted
|
||||
const isEventEncrypted = event.type === "m.room.encrypted";
|
||||
const isRoomEncrypted = this._client.isRoomEncrypted(this.roomId);
|
||||
return isEventEncrypted || !isRoomEncrypted;
|
||||
});
|
||||
|
||||
const { store } = this._client._sessionStore;
|
||||
if (this._pendingEventList.length > 0) {
|
||||
store.setItem(
|
||||
pendingEventsKey(this.roomId),
|
||||
JSON.stringify(pendingEvents),
|
||||
);
|
||||
} else {
|
||||
store.removeItem(pendingEventsKey(this.roomId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to aggregate the local echo for a relation, and also
|
||||
* for re-applying a relation after it's redaction has been cancelled,
|
||||
@@ -1310,12 +1405,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this._pendingEventList) {
|
||||
utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) {
|
||||
return ev.getId() == oldEventId;
|
||||
}, false,
|
||||
);
|
||||
this.removePendingEvent(oldEventId);
|
||||
}
|
||||
|
||||
// replace the event source (this will preserve the plaintext payload if
|
||||
@@ -1434,6 +1524,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
}
|
||||
this.removeEvent(oldEventId);
|
||||
}
|
||||
this._savePendingEvents();
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
|
||||
};
|
||||
@@ -1872,7 +1963,7 @@ Room.prototype.getJoinRule = function() {
|
||||
Room.prototype.getType = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) {
|
||||
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
|
||||
logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
|
||||
return undefined;
|
||||
}
|
||||
return createEvent.getContent()[RoomCreateTypeField];
|
||||
|
||||
@@ -268,6 +268,10 @@ export function PushProcessor(client) {
|
||||
return cond.value === val;
|
||||
}
|
||||
|
||||
if (typeof cond.pattern !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let regex;
|
||||
|
||||
if (cond.key == 'content.body') {
|
||||
|
||||
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
|
||||
import {MemoryStore} from "./memory";
|
||||
import * as utils from "../utils";
|
||||
|
||||
+1
-1
@@ -476,7 +476,7 @@ SyncApi.prototype.sync = function() {
|
||||
|
||||
this._running = true;
|
||||
|
||||
if (global.window) {
|
||||
if (global.window && global.window.addEventListener) {
|
||||
this._onOnlineBound = this._onOnline.bind(this);
|
||||
global.window.addEventListener("online", this._onOnlineBound, false);
|
||||
}
|
||||
|
||||
+161
-249
@@ -28,7 +28,15 @@ import MatrixEvent from '../models/event';
|
||||
import {EventType} from '../@types/event';
|
||||
import { RoomMember } from '../models/room-member';
|
||||
import { randomString } from '../randomstring';
|
||||
import { MCallReplacesEvent, MCallAnswer, MCallOfferNegotiate, CallCapabilities } from './callEventTypes';
|
||||
import {
|
||||
MCallReplacesEvent,
|
||||
MCallAnswer,
|
||||
MCallOfferNegotiate,
|
||||
CallCapabilities,
|
||||
SDPStreamMetadataPurpose,
|
||||
} from './callEventTypes';
|
||||
import { CallFeed } from './callFeed';
|
||||
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
|
||||
@@ -62,6 +70,11 @@ interface TurnServer {
|
||||
ttl?: number,
|
||||
}
|
||||
|
||||
interface AssertedIdentity {
|
||||
id: string,
|
||||
displayName: string,
|
||||
}
|
||||
|
||||
export enum CallState {
|
||||
Fledgling = 'fledgling',
|
||||
InviteSent = 'invite_sent',
|
||||
@@ -101,6 +114,10 @@ export enum CallEvent {
|
||||
RemoteHoldUnhold = 'remote_hold_unhold',
|
||||
// backwards compat alias for LocalHoldUnhold: remove in a major version bump
|
||||
HoldUnhold = 'hold_unhold',
|
||||
// Feeds have changed
|
||||
FeedsChanged = 'feeds_changed',
|
||||
|
||||
AssertedIdentityChanged = 'asserted_identity_changed',
|
||||
}
|
||||
|
||||
export enum CallErrorCode {
|
||||
@@ -206,9 +223,9 @@ export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource
|
||||
}
|
||||
|
||||
export class CallError extends Error {
|
||||
code : string;
|
||||
code: string;
|
||||
|
||||
constructor(code : CallErrorCode, msg: string, err: Error) {
|
||||
constructor(code: CallErrorCode, msg: string, err: Error) {
|
||||
// Stil ldon't think there's any way to have proper nested errors
|
||||
super(msg + ": " + err);
|
||||
|
||||
@@ -248,11 +265,8 @@ export class MatrixCall extends EventEmitter {
|
||||
private candidateSendTries: number;
|
||||
private sentEndOfCandidates: boolean;
|
||||
private peerConn: RTCPeerConnection;
|
||||
private localVideoElement: HTMLVideoElement;
|
||||
private remoteVideoElement: HTMLVideoElement;
|
||||
private remoteAudioElement: HTMLAudioElement;
|
||||
private feeds: Array<CallFeed>;
|
||||
private screenSharingStream: MediaStream;
|
||||
private remoteStream: MediaStream;
|
||||
private localAVStream: MediaStream;
|
||||
private inviteOrAnswerSent: boolean;
|
||||
private waitForLocalAVStream: boolean;
|
||||
@@ -292,6 +306,8 @@ export class MatrixCall extends EventEmitter {
|
||||
// the call) we buffer them up here so we can then add the ones from the party we pick
|
||||
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
|
||||
|
||||
private remoteAssertedIdentity: AssertedIdentity;
|
||||
|
||||
constructor(opts: CallOpts) {
|
||||
super();
|
||||
this.roomId = opts.roomId;
|
||||
@@ -327,6 +343,8 @@ export class MatrixCall extends EventEmitter {
|
||||
this.unholdingRemote = false;
|
||||
this.micMuted = false;
|
||||
this.vidMuted = false;
|
||||
|
||||
this.feeds = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,51 +355,38 @@ export class MatrixCall extends EventEmitter {
|
||||
logger.debug("placeVoiceCall");
|
||||
this.checkForErrorListener();
|
||||
const constraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
this.type = CallType.Voice;
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a video call to this room.
|
||||
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></code> DOM element
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
|
||||
async placeVideoCall() {
|
||||
logger.debug("placeVideoCall");
|
||||
this.checkForErrorListener();
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
const constraints = getUserMediaContraints(ConstraintsType.Video);
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
this.type = CallType.Video;
|
||||
await this.placeCallWithConstraints(constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
async placeScreenSharingCall(
|
||||
remoteVideoElement: HTMLVideoElement,
|
||||
localVideoElement: HTMLVideoElement,
|
||||
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
|
||||
) {
|
||||
async placeScreenSharingCall(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
|
||||
logger.debug("placeScreenSharingCall");
|
||||
this.checkForErrorListener();
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
|
||||
try {
|
||||
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
|
||||
if (!screenshareConstraints) return;
|
||||
if (!screenshareConstraints) {
|
||||
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electron?.getDesktopCapturerSources) {
|
||||
// We are using Electron
|
||||
logger.debug("Getting screen stream using getUserMedia()...");
|
||||
@@ -402,6 +407,7 @@ export class MatrixCall extends EventEmitter {
|
||||
"Failed to get screen-sharing stream: ", err,
|
||||
),
|
||||
);
|
||||
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
|
||||
}
|
||||
this.type = CallType.Video;
|
||||
}
|
||||
@@ -414,86 +420,58 @@ export class MatrixCall extends EventEmitter {
|
||||
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the local <code><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
public getLocalVideoElement(): HTMLVideoElement {
|
||||
return this.localVideoElement;
|
||||
public getRemoteAssertedIdentity(): AssertedIdentity {
|
||||
return this.remoteAssertedIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><video></code> DOM element
|
||||
* used for playing back video capable streams.
|
||||
* @return {Element} The dom element
|
||||
* Returns an array of all CallFeeds
|
||||
* @returns {Array<CallFeed>} CallFeeds
|
||||
*/
|
||||
public getRemoteVideoElement(): HTMLVideoElement {
|
||||
return this.remoteVideoElement;
|
||||
public getFeeds(): Array<CallFeed> {
|
||||
return this.feeds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><audio></code> DOM element
|
||||
* used for playing back audio only streams.
|
||||
* @return {Element} The dom element
|
||||
* Returns an array of all local CallFeeds
|
||||
* @returns {Array<CallFeed>} local CallFeeds
|
||||
*/
|
||||
public getRemoteAudioElement(): HTMLAudioElement {
|
||||
return this.remoteAudioElement;
|
||||
public getLocalFeeds(): Array<CallFeed> {
|
||||
return this.feeds.filter((feed) => {return feed.isLocal()});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local <code><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
* Returns an array of all remote CallFeeds
|
||||
* @returns {Array<CallFeed>} remote CallFeeds
|
||||
*/
|
||||
public async setLocalVideoElement(element : HTMLVideoElement) {
|
||||
this.localVideoElement = element;
|
||||
public getRemoteFeeds(): Array<CallFeed> {
|
||||
return this.feeds.filter((feed) => {return !feed.isLocal()});
|
||||
}
|
||||
|
||||
if (element && this.localAVStream && this.type === CallType.Video) {
|
||||
element.autoplay = true;
|
||||
/**
|
||||
* Returns true if there are no incoming feeds,
|
||||
* otherwise returns false
|
||||
* @returns {boolean} no incoming feeds
|
||||
*/
|
||||
public noIncomingFeeds(): boolean {
|
||||
return !this.feeds.some((feed) => !feed.isLocal());
|
||||
}
|
||||
|
||||
element.srcObject = this.localAVStream;
|
||||
element.muted = true;
|
||||
try {
|
||||
await element.play();
|
||||
} catch (e) {
|
||||
logger.info("Failed to play local video element", e);
|
||||
}
|
||||
private pushNewFeed(stream: MediaStream, userId: string, purpose: SDPStreamMetadataPurpose) {
|
||||
// Try to find a feed with the same stream id as the new stream,
|
||||
// if we find it replace the old stream with the new one
|
||||
const feed = this.feeds.find((feed) => feed.stream.id === stream.id);
|
||||
if (feed) {
|
||||
feed.setNewStream(stream);
|
||||
} else {
|
||||
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId));
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the remote <code><video></code> DOM element. If this call is active,
|
||||
* the first received video-capable stream will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
public setRemoteVideoElement(element : HTMLVideoElement) {
|
||||
if (element === this.remoteVideoElement) return;
|
||||
|
||||
element.autoplay = true;
|
||||
|
||||
// if we already have an audio element set, use that instead and mute the audio
|
||||
// on this video element.
|
||||
if (this.remoteAudioElement) element.muted = true;
|
||||
|
||||
this.remoteVideoElement = element;
|
||||
|
||||
if (this.remoteStream) {
|
||||
this.playRemoteVideo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the remote <code><audio></code> DOM element. If this call is active,
|
||||
* the first received audio-only stream will be rendered to it immediately.
|
||||
* The audio will *not* be rendered from the remoteVideoElement.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
public async setRemoteAudioElement(element: HTMLAudioElement) {
|
||||
if (element === this.remoteAudioElement) return;
|
||||
|
||||
this.remoteAudioElement = element;
|
||||
|
||||
if (this.remoteStream) this.playRemoteAudio();
|
||||
private deleteAllFeeds() {
|
||||
this.feeds = [];
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
|
||||
// The typescript definitions have this type as 'any' :(
|
||||
@@ -548,16 +526,18 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteStream = this.feeds.find((feed) => {return !feed.isLocal()})?.stream;
|
||||
|
||||
// According to previous comments in this file, firefox at some point did not
|
||||
// add streams until media started ariving on them. Testing latest firefox
|
||||
// (81 at time of writing), this is no longer a problem, so let's do it the correct way.
|
||||
if (!this.remoteStream || this.remoteStream.getTracks().length === 0) {
|
||||
if (!remoteStream || remoteStream.getTracks().length === 0) {
|
||||
logger.error("No remote stream or no tracks after setting remote description!");
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||
this.type = remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||
|
||||
this.setState(CallState.Ringing);
|
||||
|
||||
@@ -642,9 +622,6 @@ export class MatrixCall extends EventEmitter {
|
||||
newCall.gotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
}
|
||||
newCall.localVideoElement = this.localVideoElement;
|
||||
newCall.remoteVideoElement = this.remoteVideoElement;
|
||||
newCall.remoteAudioElement = this.remoteAudioElement;
|
||||
this.successor = newCall;
|
||||
this.emit(CallEvent.Replaced, newCall);
|
||||
this.hangup(CallErrorCode.Replaced, true);
|
||||
@@ -756,10 +733,6 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
this.updateMuteStatus();
|
||||
|
||||
if (!onHold) {
|
||||
this.playRemoteAudio();
|
||||
}
|
||||
|
||||
this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold);
|
||||
}
|
||||
|
||||
@@ -813,16 +786,6 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
const vidShouldBeMuted = this.vidMuted || this.remoteOnHold;
|
||||
setTracksEnabled(this.localAVStream.getVideoTracks(), !vidShouldBeMuted);
|
||||
|
||||
if (this.remoteOnHold) {
|
||||
if (this.remoteAudioElement && this.remoteAudioElement.srcObject === this.remoteStream) {
|
||||
this.remoteAudioElement.muted = true;
|
||||
} else if (this.remoteVideoElement && this.remoteVideoElement.srcObject === this.remoteStream) {
|
||||
this.remoteVideoElement.muted = true;
|
||||
}
|
||||
} else {
|
||||
this.playRemoteAudio();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -845,24 +808,13 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
logger.debug("gotUserMediaForInvite -> " + this.type);
|
||||
|
||||
const videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type === CallType.Video) {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
logger.debug(
|
||||
"Setting screen sharing stream to the local video element",
|
||||
);
|
||||
videoEl.srcObject = this.screenSharingStream;
|
||||
} else {
|
||||
videoEl.srcObject = stream;
|
||||
}
|
||||
videoEl.muted = true;
|
||||
try {
|
||||
await videoEl.play();
|
||||
} catch (e) {
|
||||
logger.info("Failed to play local video element", e);
|
||||
}
|
||||
if (this.screenSharingStream) {
|
||||
logger.debug(
|
||||
"Setting screen sharing stream to the local video element",
|
||||
);
|
||||
this.pushNewFeed(this.screenSharingStream, this.client.getUserId(), SDPStreamMetadataPurpose.Screenshare);
|
||||
} else {
|
||||
this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia);
|
||||
}
|
||||
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
@@ -932,19 +884,7 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const localVidEl = this.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && this.type === CallType.Video) {
|
||||
localVidEl.autoplay = true;
|
||||
localVidEl.srcObject = stream;
|
||||
|
||||
localVidEl.muted = true;
|
||||
try {
|
||||
await localVidEl.play();
|
||||
} catch (e) {
|
||||
logger.info("Failed to play local video element", e);
|
||||
}
|
||||
}
|
||||
this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia);
|
||||
|
||||
this.localAVStream = stream;
|
||||
logger.info("Got local AV stream with id " + this.localAVStream.id);
|
||||
@@ -1194,7 +1134,17 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private callHasEnded() : boolean {
|
||||
async onAssertedIdentityReceived(event: MatrixEvent) {
|
||||
if (!event.getContent().asserted_identity) return;
|
||||
|
||||
this.remoteAssertedIdentity = {
|
||||
id: event.getContent().asserted_identity.id,
|
||||
displayName: event.getContent().asserted_identity.display_name,
|
||||
};
|
||||
this.emit(CallEvent.AssertedIdentityChanged);
|
||||
}
|
||||
|
||||
private callHasEnded(): boolean {
|
||||
// This exists as workaround to typescript trying to be clever and erroring
|
||||
// when putting if (this.state === CallState.Ended) return; twice in the same
|
||||
// function, even though that function is async.
|
||||
@@ -1349,32 +1299,31 @@ export class MatrixCall extends EventEmitter {
|
||||
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldRemoteStream = this.feeds.find((feed) => {return !feed.isLocal()})?.stream;
|
||||
|
||||
// If we already have a stream, check this track is from the same one
|
||||
if (this.remoteStream && ev.streams[0].id !== this.remoteStream.id) {
|
||||
// Note that we check by ID and always set the remote stream: Chrome appears
|
||||
// to make new stream objects when tranciever directionality is changed and the 'active'
|
||||
// status of streams change - Dave
|
||||
if (oldRemoteStream && ev.streams[0].id !== oldRemoteStream.id) {
|
||||
logger.warn(
|
||||
`Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${this.remoteStream.id}`,
|
||||
`Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${oldRemoteStream.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.remoteStream) {
|
||||
if (!oldRemoteStream) {
|
||||
logger.info("Got remote stream with id " + ev.streams[0].id);
|
||||
}
|
||||
|
||||
// Note that we check by ID above and always set the remote stream: Chrome appears
|
||||
// to make new stream objects when tranciever directionality is changed and the 'active'
|
||||
// status of streams change
|
||||
this.remoteStream = ev.streams[0];
|
||||
const newRemoteStream = ev.streams[0];
|
||||
|
||||
logger.debug(`Track id ${ev.track.id} of kind ${ev.track.kind} added`);
|
||||
|
||||
if (ev.track.kind === 'video') {
|
||||
if (this.remoteVideoElement) {
|
||||
this.playRemoteVideo();
|
||||
}
|
||||
} else {
|
||||
if (this.remoteAudioElement) this.playRemoteAudio();
|
||||
}
|
||||
this.pushNewFeed(newRemoteStream, this.getOpponentMember().userId, SDPStreamMetadataPurpose.Usermedia)
|
||||
|
||||
logger.info("playing remote. stream active? " + newRemoteStream.active);
|
||||
};
|
||||
|
||||
onNegotiationNeeded = async () => {
|
||||
@@ -1397,52 +1346,6 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
async playRemoteAudio() {
|
||||
if (this.remoteVideoElement) this.remoteVideoElement.muted = true;
|
||||
this.remoteAudioElement.muted = false;
|
||||
|
||||
this.remoteAudioElement.srcObject = this.remoteStream;
|
||||
|
||||
// if audioOutput is non-default:
|
||||
try {
|
||||
if (audioOutput) {
|
||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||
// it fails.
|
||||
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
|
||||
// back to the default after the call is over
|
||||
logger.info("Setting audio sink to " + audioOutput + ", was " + this.remoteAudioElement.sinkId);
|
||||
await this.remoteAudioElement.setSinkId(audioOutput);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.remoteAudioElement.play();
|
||||
} catch (e) {
|
||||
logger.error("Failed to play remote audio element", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async playRemoteVideo() {
|
||||
// A note on calling methods on media elements:
|
||||
// We used to have queues per media element to serialise all calls on those elements.
|
||||
// The reason given for this was that load() and play() were racing. However, we now
|
||||
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||
// operation was causing bugs where video would not resume because some play command
|
||||
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||
// them with another load() which will cancel the pending one, but since we don't call
|
||||
// load() explicitly, it shouldn't be a problem.
|
||||
this.remoteVideoElement.srcObject = this.remoteStream;
|
||||
logger.info("playing remote video. stream active? " + this.remoteStream.active);
|
||||
try {
|
||||
await this.remoteVideoElement.play();
|
||||
} catch (e) {
|
||||
logger.info("Failed to play remote video element", e);
|
||||
}
|
||||
}
|
||||
|
||||
onHangupReceived = (msg) => {
|
||||
logger.debug("Hangup received for call ID " + this.callId);
|
||||
|
||||
@@ -1527,7 +1430,10 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async transfer(targetUserId: string, targetRoomId?: string) {
|
||||
/*
|
||||
* Transfers this call to another user
|
||||
*/
|
||||
async transfer(targetUserId: string) {
|
||||
// Fetch the target user's global profile info: their room avatar / displayname
|
||||
// could be different in whatever room we shae with them.
|
||||
const profileInfo = await this.client.getProfileInfo(targetUserId);
|
||||
@@ -1544,9 +1450,49 @@ export class MatrixCall extends EventEmitter {
|
||||
create_call: replacementId,
|
||||
} as MCallReplacesEvent;
|
||||
|
||||
if (targetRoomId) body.target_room = targetRoomId;
|
||||
await this.sendVoipEvent(EventType.CallReplaces, body);
|
||||
|
||||
return this.sendVoipEvent(EventType.CallReplaces, body);
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Transfers this call to the target call, effectively 'joining' the
|
||||
* two calls (so the remote parties on each call are connected together).
|
||||
*/
|
||||
async transferToCall(transferTargetCall?: MatrixCall) {
|
||||
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
|
||||
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
|
||||
|
||||
const newCallId = genCallID();
|
||||
|
||||
const bodyToTransferTarget = {
|
||||
// the replacements on each side have their own ID, and it's distinct from the
|
||||
// ID of the new call (but we can use the same function to generate it)
|
||||
replacement_id: genCallID(),
|
||||
target_user: {
|
||||
id: this.getOpponentMember().userId,
|
||||
display_name: transfereeProfileInfo.display_name,
|
||||
avatar_url: transfereeProfileInfo.avatar_url,
|
||||
},
|
||||
await_call: newCallId,
|
||||
} as MCallReplacesEvent;
|
||||
|
||||
await transferTargetCall.sendVoipEvent(EventType.CallReplaces, bodyToTransferTarget);
|
||||
|
||||
const bodyToTransferee = {
|
||||
replacement_id: genCallID(),
|
||||
target_user: {
|
||||
id: transferTargetCall.getOpponentMember().userId,
|
||||
display_name: targetProfileInfo.display_name,
|
||||
avatar_url: targetProfileInfo.avatar_url,
|
||||
},
|
||||
create_call: newCallId,
|
||||
} as MCallReplacesEvent;
|
||||
|
||||
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
|
||||
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
|
||||
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Replaced, true);
|
||||
}
|
||||
|
||||
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
|
||||
@@ -1559,33 +1505,14 @@ export class MatrixCall extends EventEmitter {
|
||||
this.inviteTimeout = null;
|
||||
}
|
||||
|
||||
const remoteVid = this.getRemoteVideoElement();
|
||||
const remoteAud = this.getRemoteAudioElement();
|
||||
const localVid = this.getLocalVideoElement();
|
||||
// Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds()
|
||||
// We don't stop media if the call was replaced as we want to re-use streams in the successor
|
||||
if (hangupReason !== CallErrorCode.Replaced) this.stopAllMedia();
|
||||
this.deleteAllFeeds();
|
||||
|
||||
if (remoteVid) {
|
||||
remoteVid.pause();
|
||||
remoteVid.srcObject = null;
|
||||
}
|
||||
if (remoteAud) {
|
||||
remoteAud.pause();
|
||||
remoteAud.srcObject = null;
|
||||
try {
|
||||
// As per comment in playRemoteAudio, setting the sink ID back to the default
|
||||
// once the call is over makes setSinkId work reliably.
|
||||
await this.remoteAudioElement.setSinkId('')
|
||||
} catch (e) {
|
||||
logger.warn("Failed to set sink ID back to default");
|
||||
}
|
||||
}
|
||||
if (localVid) {
|
||||
localVid.pause();
|
||||
localVid.srcObject = null;
|
||||
}
|
||||
this.hangupParty = hangupParty;
|
||||
this.hangupReason = hangupReason;
|
||||
this.setState(CallState.Ended);
|
||||
this.stopAllMedia();
|
||||
if (this.peerConn && this.peerConn.signalingState !== 'closed') {
|
||||
this.peerConn.close();
|
||||
}
|
||||
@@ -1596,19 +1523,9 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
private stopAllMedia() {
|
||||
logger.debug(`stopAllMedia (stream=${this.localAVStream})`);
|
||||
if (this.localAVStream) {
|
||||
for (const track of this.localAVStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
if (this.screenSharingStream) {
|
||||
for (const track of this.screenSharingStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.remoteStream) {
|
||||
for (const track of this.remoteStream.getTracks()) {
|
||||
for (const feed of this.feeds) {
|
||||
for (const track of feed.stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
@@ -1839,16 +1756,8 @@ async function getScreenshareContraints(selectDesktopCapturerSource?: () => Prom
|
||||
}
|
||||
}
|
||||
|
||||
let audioOutput: string;
|
||||
let audioInput: string;
|
||||
let videoInput: string;
|
||||
/**
|
||||
* Set an audio output device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
export function setAudioOutput(deviceId: string) { audioOutput = deviceId; }
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
@@ -1865,6 +1774,9 @@ export function setAudioInput(deviceId: string) { audioInput = deviceId; }
|
||||
export function setVideoInput(deviceId: string) { videoInput = deviceId; }
|
||||
|
||||
/**
|
||||
* DEPRECATED
|
||||
* Use client.createCall()
|
||||
*
|
||||
* Create a new Matrix call for the browser.
|
||||
* @param {MatrixClient} client The client instance to use.
|
||||
* @param {string} roomId The room the call is in.
|
||||
|
||||
@@ -87,7 +87,11 @@ export class CallEventHandler {
|
||||
|
||||
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()) {
|
||||
if (
|
||||
event.getType().indexOf("m.call.") === 0 ||
|
||||
event.getType().indexOf("org.matrix.call.") === 0
|
||||
|| event.isBeingDecrypted()
|
||||
) {
|
||||
// queue up for processing once all events from this sync have been
|
||||
// processed (see above).
|
||||
this.callEventBuffer.push(event);
|
||||
@@ -271,6 +275,18 @@ export class CallEventHandler {
|
||||
}
|
||||
|
||||
call.onNegotiateReceived(event);
|
||||
} else if (
|
||||
event.getType() === EventType.CallAssertedIdentity ||
|
||||
event.getType() === EventType.CallAssertedIdentityPrefix
|
||||
) {
|
||||
if (!call) return;
|
||||
|
||||
if (event.getContent().party_id === call.ourPartyId) {
|
||||
// Ignore remote echo (not that we send asserted identity, but still...)
|
||||
return;
|
||||
}
|
||||
|
||||
call.onAssertedIdentityReceived(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// allow camelcase as these are events type that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export enum SDPStreamMetadataPurpose {
|
||||
Usermedia = "m.usermedia",
|
||||
Screenshare = "m.screenshare",
|
||||
}
|
||||
|
||||
interface CallOfferAnswer {
|
||||
type: string;
|
||||
sdp: string;
|
||||
@@ -32,6 +37,7 @@ export interface MCallReplacesEvent {
|
||||
replacement_id: string;
|
||||
target_user: MCallReplacesTarget;
|
||||
create_call: string;
|
||||
await_call: string;
|
||||
target_room: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 EventEmitter from "events";
|
||||
import {SDPStreamMetadataPurpose} from "./callEventTypes";
|
||||
import MatrixClient from "../client"
|
||||
import {RoomMember} from "../models/room-member";
|
||||
|
||||
export enum CallFeedEvent {
|
||||
NewStream = "new_stream",
|
||||
}
|
||||
|
||||
export class CallFeed extends EventEmitter {
|
||||
constructor(
|
||||
public stream: MediaStream,
|
||||
public userId: string,
|
||||
public purpose: SDPStreamMetadataPurpose,
|
||||
private client: MatrixClient,
|
||||
private roomId: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns callRoom member
|
||||
* @returns member of the callRoom
|
||||
*/
|
||||
public getMember(): RoomMember {
|
||||
const callRoom = this.client.getRoom(this.roomId);
|
||||
return callRoom.getMember(this.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if CallFeed is local, otherwise returns false
|
||||
* @returns {boolean} is local?
|
||||
*/
|
||||
public isLocal(): boolean {
|
||||
return this.userId === this.client.getUserId();
|
||||
}
|
||||
|
||||
// TODO: The two following methods should be later replaced
|
||||
// by something that will also check if the remote is muted
|
||||
/**
|
||||
* Returns true if audio is muted or if there are no audio
|
||||
* tracks, otherwise returns false
|
||||
* @returns {boolean} is audio muted?
|
||||
*/
|
||||
public isAudioMuted(): boolean {
|
||||
return this.stream.getAudioTracks().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true video is muted or if there are no video
|
||||
* tracks, otherwise returns false
|
||||
* @returns {boolean} is video muted?
|
||||
*/
|
||||
public isVideoMuted(): boolean {
|
||||
// We assume only one video track
|
||||
return this.stream.getVideoTracks().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current MediaStream with a new one.
|
||||
* This method should be only used by MatrixCall.
|
||||
* @param newStream new stream with which to replace the current one
|
||||
*/
|
||||
public setNewStream(newStream: MediaStream) {
|
||||
this.stream = newStream;
|
||||
this.emit(CallFeedEvent.NewStream, this.stream);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user