Compare commits
160 Commits
v9.4.0-rc.2
...
v9.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dd60de57d | |||
| 5efbfc2dba | |||
| 6a7d58e22e | |||
| 203829c1cd | |||
| b55e6c4ef0 | |||
| 8d779e8aec | |||
| dd1d48f688 | |||
| 8d14dc9ee3 | |||
| a8b9d8e3ae | |||
| 83d1e61b2f | |||
| f547fa732f | |||
| e24b1519a4 | |||
| 3028fe9c87 | |||
| 0b970b05b6 | |||
| f7bfb1e49e | |||
| 371ca009e9 | |||
| 4a0f848551 | |||
| 5e8b7b2a62 | |||
| 0f27b703bd | |||
| 61e19c30cb | |||
| c82bc35202 | |||
| 65934227c3 | |||
| 7519becd43 | |||
| fe83c15bc6 | |||
| 07e6b47fa7 | |||
| b026e1c2f7 | |||
| f8194d9418 | |||
| 1ecd7f274f | |||
| 66bf0ec7af | |||
| 1b22df2b7b | |||
| 9f993f1f67 | |||
| 975518bd88 | |||
| 66a863456c | |||
| 91290c0d25 | |||
| 8a23e89c87 | |||
| 9e9cf85ba1 | |||
| 3dd365bbea | |||
| 33a824b980 | |||
| 8571884304 | |||
| 4f1067e66c | |||
| 7b5b851db0 | |||
| ed0be0cf84 | |||
| d3775e5cb1 | |||
| 2c8f658810 | |||
| 5c52f5f579 | |||
| 0a81bb3fdc | |||
| f33196bc51 | |||
| 6beb90a835 | |||
| 9d45e6acd6 | |||
| 516c464458 | |||
| 6ad3fb16b3 | |||
| 277fdd9b8c | |||
| 7d56993b39 | |||
| 4e1442fcf6 | |||
| 4777bf3e75 | |||
| 14cd37ec56 | |||
| 8bcdfd50c9 | |||
| 6776df8e80 | |||
| fbec079c9b | |||
| 93f6bc3780 | |||
| dde8f23cc3 | |||
| 7cfbd0da95 | |||
| a27ddfaaaf | |||
| b53f616015 | |||
| 22dc175879 | |||
| 5f23e4699c | |||
| a1bd258a7b | |||
| 39a9c54589 | |||
| 5f68370e07 | |||
| dae2de703d | |||
| fa19c40868 | |||
| 1df69d259a | |||
| 90dda0ca68 | |||
| e2d138cac6 | |||
| 15f968d5f8 | |||
| 4a3b68de8f | |||
| f6aec7f763 | |||
| 212b6c3a0f | |||
| 820256d451 | |||
| 3aba538db3 | |||
| 4820cf8cac | |||
| c289effba0 | |||
| 3edccf496a | |||
| 97b4171b3e | |||
| 4a073a7ba5 | |||
| 9f275d57a9 | |||
| d23bbaeb06 | |||
| c64f7a9ec4 | |||
| 214a9df382 | |||
| 90f6620f1e | |||
| 45f3a2f909 | |||
| 5904378170 | |||
| f6e8048d9e | |||
| 5afca17d27 | |||
| 2d7f5ae279 | |||
| 458384d658 | |||
| 159b98132d | |||
| 349bb2730a | |||
| c13813348d | |||
| 26e70d6b30 | |||
| f6d3b50b08 | |||
| 50ee489079 | |||
| b60e5f40ab | |||
| 5b1fdb7b37 | |||
| 65d4015300 | |||
| b692cd109e | |||
| 0f90f055ba | |||
| 28d5ce288c | |||
| c701bf279f | |||
| 6039066e7f | |||
| c9a2f8b170 | |||
| b34a36d853 | |||
| f8f76f6806 | |||
| e25ae546fc | |||
| 5b73bf3e5d | |||
| c16b093bd7 | |||
| e406f32386 | |||
| c4e7c149a4 | |||
| f91edfabbb | |||
| f410004d45 | |||
| 49e238d580 | |||
| 489d188966 | |||
| 79fb7bab0b | |||
| c410954bad | |||
| 53a8a7d50f | |||
| 1c7f95c0ee | |||
| 1717fcf499 | |||
| b25453cf87 | |||
| 07b596bf30 | |||
| 1166947c21 | |||
| 6e7b9ca6c0 | |||
| 5b1e3537cc | |||
| 44843d418d | |||
| a103ffa038 | |||
| 712335789e | |||
| cdf8186f44 | |||
| d06942d602 | |||
| 45ac3a60dc | |||
| 4f244da3ec | |||
| 4eefa05d3f | |||
| 40fb31099a | |||
| bcd85f5397 | |||
| 89aeda45c6 | |||
| 7581e5ffdc | |||
| 7046fa3224 | |||
| ef392785e8 | |||
| 5bd029115c | |||
| b6f42b25dd | |||
| c0a88b7f4e | |||
| 150e5fede4 | |||
| bb3ec322fb | |||
| f3ee164a7d | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe |
@@ -35,6 +35,8 @@ module.exports = {
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// While we're converting to ts we make heavy use of this
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"quotes": "off",
|
||||
|
||||
+148
@@ -1,3 +1,151 @@
|
||||
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
|
||||
|
||||
* Optimise prefixed logger
|
||||
[\#1615](https://github.com/matrix-org/matrix-js-sdk/pull/1615)
|
||||
* Add debug logs to encryption prep, take 3
|
||||
[\#1614](https://github.com/matrix-org/matrix-js-sdk/pull/1614)
|
||||
* Add functions for upper & lowercase random strings
|
||||
[\#1612](https://github.com/matrix-org/matrix-js-sdk/pull/1612)
|
||||
* Room helpers for invite permissions and join rules
|
||||
[\#1609](https://github.com/matrix-org/matrix-js-sdk/pull/1609)
|
||||
* Fixed wording in "Adding video track with id" log
|
||||
[\#1606](https://github.com/matrix-org/matrix-js-sdk/pull/1606)
|
||||
* Add more debug logs to encryption prep
|
||||
[\#1605](https://github.com/matrix-org/matrix-js-sdk/pull/1605)
|
||||
* Add option to set ice candidate pool size
|
||||
[\#1604](https://github.com/matrix-org/matrix-js-sdk/pull/1604)
|
||||
* Cancel call if no source was selected
|
||||
[\#1601](https://github.com/matrix-org/matrix-js-sdk/pull/1601)
|
||||
|
||||
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.7.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0-rc.1) (2021-02-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0...v9.7.0-rc.1)
|
||||
|
||||
* Handle undefined peerconn
|
||||
[\#1600](https://github.com/matrix-org/matrix-js-sdk/pull/1600)
|
||||
* ReEmitter: Don't throw if no error handler is attached
|
||||
[\#1599](https://github.com/matrix-org/matrix-js-sdk/pull/1599)
|
||||
* Convert ReEmitter to TS
|
||||
[\#1598](https://github.com/matrix-org/matrix-js-sdk/pull/1598)
|
||||
* Fix typo in main readme
|
||||
[\#1597](https://github.com/matrix-org/matrix-js-sdk/pull/1597)
|
||||
* Remove rogue plus character
|
||||
[\#1596](https://github.com/matrix-org/matrix-js-sdk/pull/1596)
|
||||
* Fix call ID NaN
|
||||
[\#1595](https://github.com/matrix-org/matrix-js-sdk/pull/1595)
|
||||
* Fix Electron type merging
|
||||
[\#1594](https://github.com/matrix-org/matrix-js-sdk/pull/1594)
|
||||
* Fix browser screen share
|
||||
[\#1593](https://github.com/matrix-org/matrix-js-sdk/pull/1593)
|
||||
* Fix desktop Matrix screen sharing
|
||||
[\#1570](https://github.com/matrix-org/matrix-js-sdk/pull/1570)
|
||||
* Guard against confused server retry times
|
||||
[\#1591](https://github.com/matrix-org/matrix-js-sdk/pull/1591)
|
||||
* Decrypt redaction events
|
||||
[\#1589](https://github.com/matrix-org/matrix-js-sdk/pull/1589)
|
||||
* Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1587](https://github.com/matrix-org/matrix-js-sdk/pull/1587)
|
||||
|
||||
Changes in [9.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0) (2021-02-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0-rc.1...v9.6.0)
|
||||
|
||||
* [Release] Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1588](https://github.com/matrix-org/matrix-js-sdk/pull/1588)
|
||||
|
||||
Changes in [9.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0-rc.1) (2021-01-29)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.1...v9.6.0-rc.1)
|
||||
|
||||
* Add support for getting call stats
|
||||
[\#1584](https://github.com/matrix-org/matrix-js-sdk/pull/1584)
|
||||
* Fix compatibility with v0 calls
|
||||
[\#1583](https://github.com/matrix-org/matrix-js-sdk/pull/1583)
|
||||
* Upgrade deps 2021-01
|
||||
[\#1582](https://github.com/matrix-org/matrix-js-sdk/pull/1582)
|
||||
* Log the call ID when logging that we've received VoIP events
|
||||
[\#1581](https://github.com/matrix-org/matrix-js-sdk/pull/1581)
|
||||
* Fix extra negotiate message in Firefox
|
||||
[\#1579](https://github.com/matrix-org/matrix-js-sdk/pull/1579)
|
||||
* Add debug logs to encryption prep
|
||||
[\#1580](https://github.com/matrix-org/matrix-js-sdk/pull/1580)
|
||||
* Expose getPresence endpoint
|
||||
[\#1578](https://github.com/matrix-org/matrix-js-sdk/pull/1578)
|
||||
* Queue keys for backup even if backup isn't enabled yet
|
||||
[\#1577](https://github.com/matrix-org/matrix-js-sdk/pull/1577)
|
||||
* Stop retrying TURN access when forbidden
|
||||
[\#1576](https://github.com/matrix-org/matrix-js-sdk/pull/1576)
|
||||
* Add DTMF sending support
|
||||
[\#1573](https://github.com/matrix-org/matrix-js-sdk/pull/1573)
|
||||
|
||||
Changes in [9.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.1) (2021-01-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0...v9.5.1)
|
||||
|
||||
* [Release] Fix compatibility with v0 calls
|
||||
[\#1585](https://github.com/matrix-org/matrix-js-sdk/pull/1585)
|
||||
|
||||
Changes in [9.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0) (2021-01-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0-rc.1...v9.5.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0-rc.1) (2021-01-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.1...v9.5.0-rc.1)
|
||||
|
||||
* Don't log if no WebRTC
|
||||
[\#1574](https://github.com/matrix-org/matrix-js-sdk/pull/1574)
|
||||
* Add _unstable_getSharedRooms
|
||||
[\#1417](https://github.com/matrix-org/matrix-js-sdk/pull/1417)
|
||||
* Bump node-notifier from 8.0.0 to 8.0.1
|
||||
[\#1568](https://github.com/matrix-org/matrix-js-sdk/pull/1568)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1567](https://github.com/matrix-org/matrix-js-sdk/pull/1567)
|
||||
* Basic call transfer initiation support
|
||||
[\#1566](https://github.com/matrix-org/matrix-js-sdk/pull/1566)
|
||||
* Room version 6 is now a thing
|
||||
[\#1572](https://github.com/matrix-org/matrix-js-sdk/pull/1572)
|
||||
* Store keys with same index but better trust level
|
||||
[\#1571](https://github.com/matrix-org/matrix-js-sdk/pull/1571)
|
||||
* Use TypeScript source for development, swap to build during release
|
||||
[\#1561](https://github.com/matrix-org/matrix-js-sdk/pull/1561)
|
||||
* Revert "Ignore party ID if opponent is v0"
|
||||
[\#1565](https://github.com/matrix-org/matrix-js-sdk/pull/1565)
|
||||
* Basic call transfer initiation support
|
||||
[\#1558](https://github.com/matrix-org/matrix-js-sdk/pull/1558)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1559](https://github.com/matrix-org/matrix-js-sdk/pull/1559)
|
||||
* Honour a call reject event from another of our own devices
|
||||
[\#1562](https://github.com/matrix-org/matrix-js-sdk/pull/1562)
|
||||
|
||||
Changes in [9.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.1) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0...v9.4.1)
|
||||
|
||||
* Further script tweaks to get all layers building again
|
||||
|
||||
Changes in [9.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.2...v9.4.0)
|
||||
|
||||
* Revert `postinstall` script change, causes issues for other layers
|
||||
|
||||
Changes in [9.4.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.2) (2020-12-16)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.1...v9.4.0-rc.2)
|
||||
|
||||
@@ -307,7 +307,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
+17
-15
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.4.0-rc.2",
|
||||
"version": "9.8.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -28,10 +28,11 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
@@ -53,22 +54,22 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"qs": "^6.9.4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.8",
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^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",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.7",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.1",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
@@ -77,22 +78,23 @@
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.14.0",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-matrix-org": "^0.2.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"jest-localstorage-mock": "^2.4.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.2"
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+38
@@ -178,6 +178,19 @@ echo "yarn version"
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# For the published and dist versions of the package, we copy the
|
||||
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
@@ -353,5 +366,30 @@ if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
||||
# we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
@@ -971,4 +971,60 @@ describe("megolm", function() {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", function() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(groupSession.session_key());
|
||||
const rawEvent = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
return testClient.client.initCrypto().then(() => {
|
||||
const keys = [{
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: inboundGroupSession.export_session(0),
|
||||
sender_key: testSenderKey,
|
||||
}];
|
||||
return testClient.client.importRoomKeys(keys, { untrusted: true });
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||
});
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
event._senderCurve25519Key = testSenderKey;
|
||||
return testClient.client._crypto._onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
class EventSource extends EventEmitter {
|
||||
doTheThing() {
|
||||
this.emit(EVENTNAME, "foo", "bar");
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, [EVENTNAME]);
|
||||
|
||||
src.doTheThing();
|
||||
|
||||
// Args should be the args passed to 'emit' after the event name, and
|
||||
// also the source object of the event which re-emitter adds
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
// Now we've attached an error handler, it should be called
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -521,4 +521,19 @@ describe("MatrixClient", function() {
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPresence", function() {
|
||||
it("should send a presence HTTP GET", function() {
|
||||
httpLookups = [{
|
||||
method: "GET",
|
||||
path: `/presence/${encodeURIComponent(userId)}/status`,
|
||||
data: {
|
||||
"presence": "unavailable",
|
||||
"last_active_ago": 420845,
|
||||
},
|
||||
}];
|
||||
client.getPresence(userId);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Call', function() {
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
@@ -156,7 +156,7 @@ describe('Call', function() {
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
@@ -173,7 +173,7 @@ describe('Call', function() {
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
|
||||
@@ -48,6 +48,7 @@ export enum EventType {
|
||||
CallReject = "m.call.reject",
|
||||
CallSelectAnswer = "m.call.select_answer",
|
||||
CallNegotiate = "m.call.negotiate",
|
||||
CallReplaces = "m.call.replaces",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
|
||||
Vendored
+40
-1
@@ -26,10 +26,49 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron?: Electron;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||
getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
export interface DesktopCapturerConstraints {
|
||||
audio: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
video: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
}
|
||||
|
||||
interface GetSourcesOptions {
|
||||
types: Array<string>;
|
||||
thumbnailSize?: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
fetchWindowIcons?: boolean;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
export class ReEmitter {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
|
||||
// We keep one bound event handler for each event name so we know
|
||||
// what event is arriving
|
||||
this.boundHandlers = {};
|
||||
}
|
||||
|
||||
_handleEvent(eventName, ...args) {
|
||||
this.target.emit(eventName, ...args);
|
||||
}
|
||||
|
||||
reEmit(source, eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (handler, ...args) => {
|
||||
handler(...args, source);
|
||||
};
|
||||
for (const eventName of eventNames) {
|
||||
if (this.boundHandlers[eventName] === undefined) {
|
||||
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
|
||||
}
|
||||
|
||||
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
|
||||
source.on(eventName, boundHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
for (const eventName of eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (...args) => {
|
||||
// EventEmitter special cases 'error' to make the emit function throw if no
|
||||
// handler is attached, which sort of makes sense for making sure that something
|
||||
// handles an error, but for re-emitting, there could be a listener on the original
|
||||
// source object so the test doesn't really work. We *could* try to replicate the
|
||||
// same logic and throw if there is no listener on either the source or the target,
|
||||
// but this behaviour is fairly undesireable for us anyway: the main place we throw
|
||||
// 'error' events is for calls, where error events are usually emitted some time
|
||||
// later by a different part of the code where 'emit' throwing because the app hasn't
|
||||
// added an error handler isn't terribly helpful. (A better fix in retrospect may
|
||||
// have been to just avoid using the event name 'error', but backwards compat...)
|
||||
if (eventName === 'error' && this.target.listenerCount('error') === 0) return;
|
||||
this.target.emit(eventName, ...args, source);
|
||||
};
|
||||
source.on(eventName, forSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,14 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* @param {boolean} [opts.forceTURN]
|
||||
* Optional. Whether relaying calls through a TURN server should be forced.
|
||||
*
|
||||
* * @param {boolean} [opts.iceCandidatePoolSize]
|
||||
* Optional. Up to this many ICE candidates will be gathered when an incoming call arrives.
|
||||
* Gathering does not send data to the caller, but will communicate with the configured TURN
|
||||
* server. Default 0.
|
||||
*
|
||||
* @param {boolean} [opts.supportsCallTransfer]
|
||||
* Optional. True to advertise support for call transfers to other parties on Matrix calls.
|
||||
*
|
||||
* @param {boolean} [opts.fallbackICEServerAllowed]
|
||||
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
|
||||
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
|
||||
@@ -364,6 +372,8 @@ export function MatrixClient(opts) {
|
||||
this._cryptoCallbacks = opts.cryptoCallbacks || {};
|
||||
|
||||
this._forceTURN = opts.forceTURN || false;
|
||||
this._iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
|
||||
this._supportsCallTransfer = opts.supportsCallTransfer || false;
|
||||
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
|
||||
|
||||
// List of which rooms have encryption enabled: separate from crypto because
|
||||
@@ -696,6 +706,14 @@ MatrixClient.prototype.setForceTURN = function(forceTURN) {
|
||||
this._forceTURN = forceTURN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether to advertise transfer support to other parties on Matrix calls.
|
||||
* @param {bool} supportsCallTransfer True to advertise the 'm.call.transferee' capability
|
||||
*/
|
||||
MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer) {
|
||||
this._supportsCallTransfer = supportsCallTransfer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current sync state.
|
||||
* @return {?string} the sync state, which may be null.
|
||||
@@ -3829,6 +3847,19 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} userId The user to get presence for
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: The presence state for this user.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getPresence = function(userId, callback) {
|
||||
const path = utils.encodeUri("/presence/$userId/status", {
|
||||
$userId: userId,
|
||||
});
|
||||
|
||||
return this._http.authedRequest(callback, "GET", path, undefined, undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve older messages from the given room and put them in the timeline.
|
||||
@@ -5131,6 +5162,26 @@ MatrixClient.prototype._storeClientOptions = function() {
|
||||
return this.store.storeClientOptions(serializableOpts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a set of room IDs in common with another user
|
||||
* @param {string} userId The userId to check.
|
||||
* @return {Promise<string[]>} Resolves to a set of rooms
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._unstable_getSharedRooms = async function(userId) {
|
||||
if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) {
|
||||
throw Error('Server does not support shared_rooms API');
|
||||
}
|
||||
const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", {
|
||||
$userId: userId,
|
||||
});
|
||||
const res = await this._http.authedRequest(
|
||||
undefined, "GET", path, undefined, undefined,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
return res.joined;
|
||||
};
|
||||
|
||||
/**
|
||||
* High level helper method to stop the client from polling and allow a
|
||||
* clean shutdown.
|
||||
@@ -5393,6 +5444,11 @@ function checkTurnServers(client) {
|
||||
}
|
||||
}, function(err) {
|
||||
logger.error("Failed to get TURN URIs");
|
||||
// If we get a 403, there's no point in looping forever.
|
||||
if (err.httpStatus === 403) {
|
||||
logger.info("TURN access unavailable for this account");
|
||||
return;
|
||||
}
|
||||
client._checkTurnServersTimeoutID = setTimeout(function() {
|
||||
checkTurnServers(client);
|
||||
}, 60000);
|
||||
|
||||
@@ -1059,9 +1059,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()) {
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
// decrypt more), or they have the same index and
|
||||
// the new sessions trust does not win over the old
|
||||
// sessions trust, so keep it
|
||||
logger.log(
|
||||
`Keeping existing megolm session ${sessionId}`,
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import {getPrefixedLogger, logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
@@ -264,11 +264,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
// share keys with devices that we already have a session for
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(
|
||||
session, key, payload, olmSessions,
|
||||
);
|
||||
logger.debug(`Shared keys with existing Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
const errorDevices = [];
|
||||
|
||||
// meanwhile, establish olm sessions for devices that we don't
|
||||
@@ -282,6 +285,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session, key, payload, devicesWithoutSession, errorDevices,
|
||||
singleOlmCreationPhase ? 10000 : 2000, failedServers,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
|
||||
// perform the second phase of olm session creation if requested,
|
||||
@@ -310,19 +314,24 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Sharing keys (start phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithDevices(
|
||||
session, key, payload, retryDevices, failedDevices, 30000,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
await this._notifyFailedOlmDevices(session, key, failedDevices);
|
||||
})();
|
||||
} else {
|
||||
await this._notifyFailedOlmDevices(session, key, errorDevices);
|
||||
}
|
||||
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Notifying blocked devices in ${this._roomId}`);
|
||||
// also, notify blocked devices that they're blocked
|
||||
const blockedMap = {};
|
||||
let blockedCount = 0;
|
||||
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
|
||||
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
|
||||
if (
|
||||
@@ -330,12 +339,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session.blockedDevicesNotified[userId][deviceId] === undefined
|
||||
) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
blockedMap[userId][deviceId] = {device};
|
||||
blockedMap[userId][deviceId] = { device };
|
||||
blockedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(`Notified ${blockedCount} blocked devices in ${this._roomId}`);
|
||||
})(),
|
||||
]);
|
||||
};
|
||||
@@ -348,6 +359,11 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
// first wait for the previous share to complete
|
||||
const prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// Ensure any failures are logged for debugging
|
||||
prom.catch(e => {
|
||||
logger.error(`Failed to ensure outbound session in ${this._roomId}`, e);
|
||||
});
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
@@ -369,17 +385,11 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
);
|
||||
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(sessionId);
|
||||
};
|
||||
@@ -723,13 +733,18 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = async function(
|
||||
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
|
||||
) {
|
||||
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
||||
getPrefixedLogger(`[${this._roomId}]`),
|
||||
);
|
||||
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
||||
|
||||
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
|
||||
|
||||
logger.debug(`Sharing keys with Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
|
||||
logger.debug(`Shared keys with Olm sessions in ${this._roomId}`);
|
||||
};
|
||||
|
||||
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
@@ -738,16 +753,17 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
const userDeviceMaps = this._splitDevices(devicemap);
|
||||
|
||||
for (let i = 0; i < userDeviceMaps.length; i++) {
|
||||
const taskDetail =
|
||||
`megolm keys for ${session.sessionId} ` +
|
||||
`in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
|
||||
try {
|
||||
logger.debug(`Sharing ${taskDetail}`);
|
||||
await this._encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
logger.log(`Completed megolm keyshare for ${session.sessionId} `
|
||||
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
logger.debug(`Shared ${taskDetail}`);
|
||||
} catch (e) {
|
||||
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
logger.error(`Failed to share ${taskDetail}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -766,6 +782,11 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
session, key, failedDevices,
|
||||
) {
|
||||
logger.debug(
|
||||
`Notifying ${failedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
|
||||
// mark the devices that failed as "handled" because we don't want to try
|
||||
// to claim a one-time-key for dead devices on every message.
|
||||
for (const {userId, deviceInfo} of failedDevices) {
|
||||
@@ -780,6 +801,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
await this._olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this._roomId}`,
|
||||
);
|
||||
const blockedMap = {};
|
||||
for (const {userId, deviceInfo} of filteredFailedDevices) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
@@ -797,6 +822,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
|
||||
// send the notifications
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(
|
||||
`Notified ${filteredFailedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -846,24 +875,41 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
|
||||
// We're already preparing something, so don't do anything else.
|
||||
// FIXME: check if we need to restart
|
||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||
const elapsedTime = Date.now() - this.encryptionPreparationMetadata.startTime;
|
||||
logger.debug(
|
||||
`Already started preparing to encrypt for ${this._roomId} ` +
|
||||
`${elapsedTime} ms ago, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
|
||||
|
||||
this.encryptionPreparationMetadata = {
|
||||
startTime: Date.now(),
|
||||
};
|
||||
this.encryptionPreparation = (async () => {
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
try {
|
||||
logger.debug(`Getting devices in ${this._roomId}`);
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
logger.debug(`Ensuring outbound session in ${this._roomId}`);
|
||||
await this._ensureOutboundSession(devicesInRoom, blocked, true);
|
||||
|
||||
logger.debug(`Ready to encrypt events for ${this._roomId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to prepare to encrypt events for ${this._roomId}`, e);
|
||||
} finally {
|
||||
delete this.encryptionPreparationMetadata;
|
||||
delete this.encryptionPreparation;
|
||||
}
|
||||
|
||||
await this._ensureOutboundSession(devicesInRoom, blocked, true);
|
||||
|
||||
delete this.encryptionPreparation;
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -1347,18 +1393,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
);
|
||||
}).catch((e) => {
|
||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
@@ -1564,7 +1604,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo && opts.source !== "backup") {
|
||||
if (opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
|
||||
+21
-13
@@ -57,6 +57,7 @@ import {IllegalMethod} from "./verification/IllegalMethod";
|
||||
import {KeySignatureUploadError} from "../errors";
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import {DehydrationManager} from './dehydration';
|
||||
import { MatrixEvent } from "../models/event";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -2884,18 +2885,18 @@ Crypto.prototype.backupGroupSession = async function(
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
) {
|
||||
if (!this.backupInfo) {
|
||||
throw new Error("Key backups are not enabled");
|
||||
}
|
||||
|
||||
await this._cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
if (this.backupInfo) {
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
// if this.backupInfo is not set, then the keys will be backed up when
|
||||
// client.enableKeyBackup is called
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3028,19 +3029,26 @@ Crypto.prototype.encryptEvent = async function(event, room) {
|
||||
* finished decrypting. Rejects with an `algorithms.DecryptionError` if there
|
||||
* is a problem decrypting the event.
|
||||
*/
|
||||
Crypto.prototype.decryptEvent = function(event) {
|
||||
Crypto.prototype.decryptEvent = async function(event) {
|
||||
if (event.isRedacted()) {
|
||||
return Promise.resolve({
|
||||
const redactionEvent = new MatrixEvent(event.getUnsigned().redacted_because);
|
||||
const decryptedEvent = await this.decryptEvent(redactionEvent);
|
||||
|
||||
return {
|
||||
clearEvent: {
|
||||
room_id: event.getRoomId(),
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
redacted_because: decryptedEvent.clearEvent,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
} else {
|
||||
const content = event.getWireContent();
|
||||
const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
|
||||
return await alg.decryptEvent(event);
|
||||
}
|
||||
const content = event.getWireContent();
|
||||
const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
|
||||
return alg.decryptEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+31
-8
@@ -183,18 +183,24 @@ export async function getExistingOlmSessions(
|
||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||
* failed to respond to one-time-key requests.
|
||||
*
|
||||
* @param {Object} [log] A possibly customised log
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
|
||||
) {
|
||||
if (typeof force === "number") {
|
||||
log = failedServers;
|
||||
failedServers = otkTimeout;
|
||||
otkTimeout = force;
|
||||
force = false;
|
||||
}
|
||||
if (!log) {
|
||||
log = logger;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
@@ -216,7 +222,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
// new chain when this side has an active sender chain.
|
||||
// If you see this message being logged in the wild, we should find
|
||||
// the thing that is trying to send Olm messages to itself and fix it.
|
||||
logger.info("Attempted to start session with ourself! Ignoring");
|
||||
log.info("Attempted to start session with ourself! Ignoring");
|
||||
// We must fill in the section in the return value though, as callers
|
||||
// expect it to be there.
|
||||
result[userId][deviceId] = {
|
||||
@@ -258,9 +264,9 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
|
||||
log.info(`Forcing new Olm session for ${userId}:${deviceId}`);
|
||||
} else {
|
||||
logger.info("Making new Olm session for " + userId + ":" + deviceId);
|
||||
log.info(`Making new Olm session for ${userId}:${deviceId}`);
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
@@ -277,16 +283,29 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
const oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
let res;
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
// If your homeserver takes a nap here and never replies, this process
|
||||
// would hang indefinitely. While that's easily fixed by setting a
|
||||
// timeout on this request, let's first log whether that's the root
|
||||
// cause we're seeing in practice.
|
||||
// See also https://github.com/vector-im/element-web/issues/16194
|
||||
const otkTimeoutLogger = setTimeout(() => {
|
||||
log.error(`Homeserver never replied while claiming ${taskDetail}`);
|
||||
}, otkTimeout);
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
resolver.resolve();
|
||||
}
|
||||
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
|
||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(otkTimeoutLogger);
|
||||
}
|
||||
|
||||
if (failedServers && "failures" in res) {
|
||||
@@ -323,9 +342,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId;
|
||||
logger.warn(msg);
|
||||
log.warn(
|
||||
`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` +
|
||||
`for device ${userId}:${deviceId}`,
|
||||
);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
}
|
||||
@@ -350,7 +370,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
}
|
||||
|
||||
taskDetail = `Olm sessions for ${promises.length} devices`;
|
||||
log.debug(`Starting ${taskDetail}`);
|
||||
await Promise.all(promises);
|
||||
log.debug(`Started ${taskDetail}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+20
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 André Jaenisch
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @module logger
|
||||
*/
|
||||
|
||||
import log from "loglevel";
|
||||
import log, { Logger } from "loglevel";
|
||||
|
||||
// This is to demonstrate, that you can use any namespace you want.
|
||||
// Namespaces allow you to turn on/off the logging for specific parts of the
|
||||
@@ -36,6 +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 */
|
||||
if (this.prefix) {
|
||||
args.unshift(this.prefix);
|
||||
}
|
||||
/* eslint-enable babel/no-invalid-this */
|
||||
const supportedByConsole = methodName === "error" ||
|
||||
methodName === "warn" ||
|
||||
methodName === "trace" ||
|
||||
@@ -57,3 +62,16 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
export const logger = log.getLogger(DEFAULT_NAMESPACE);
|
||||
logger.setLevel(log.levels.DEBUG);
|
||||
|
||||
interface PrefixedLogger extends Logger {
|
||||
prefix?: any;
|
||||
}
|
||||
|
||||
export function getPrefixedLogger(prefix): PrefixedLogger {
|
||||
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
|
||||
if (prefixLogger.prefix !== prefix) {
|
||||
// Only do this setup work the first time through, as loggers are saved by name.
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.setLevel(log.levels.DEBUG);
|
||||
}
|
||||
return prefixLogger;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,8 @@ export interface ICreateClientOpts {
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
verificationMethods?: Array<any>;
|
||||
forceTURN?: boolean;
|
||||
iceCandidatePoolSize?: number,
|
||||
supportsCallTransfer?: boolean,
|
||||
fallbackICEServerAllowed?: boolean;
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
}
|
||||
|
||||
@@ -811,6 +811,24 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this.getType() === "m.room.redaction";
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) redaction event JSON
|
||||
* if event was redacted
|
||||
*
|
||||
* @returns {object} The redaction event JSON, or an empty object
|
||||
*/
|
||||
getRedactionEvent: function() {
|
||||
if (!this.isRedacted()) return null;
|
||||
|
||||
if (this._clearEvent.unsigned) {
|
||||
return this._clearEvent.unsigned.redacted_because;
|
||||
} else if (this.event.unsigned.redacted_because) {
|
||||
return this.event.unsigned.redacted_because;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the push actions, if known, for this event
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ import {EventEmitter} from "events";
|
||||
import {RoomMember} from "./room-member";
|
||||
import {logger} from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import {EventType} from "../@types/event";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
const OOB_STATUS_NOTSTARTED = 1;
|
||||
@@ -718,6 +719,16 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
|
||||
return member.powerLevel >= notifLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
RoomState.prototype.getJoinRule = function() {
|
||||
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
|
||||
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
|
||||
return joinRuleContent["join_rule"] || "invite";
|
||||
};
|
||||
|
||||
|
||||
function _updateThirdPartyTokenCache(roomState, memberEvent) {
|
||||
if (!memberEvent.getContent().third_party_invite) {
|
||||
|
||||
+37
-3
@@ -30,6 +30,7 @@ import {RoomMember} from "./room-member";
|
||||
import {RoomSummary} from "./room-summary";
|
||||
import {logger} from '../logger';
|
||||
import {ReEmitter} from '../ReEmitter';
|
||||
import {EventType} from "../@types/event";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -37,8 +38,8 @@ import {ReEmitter} from '../ReEmitter';
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '5';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5'];
|
||||
const KNOWN_SAFE_ROOM_VERSION = '6';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -1822,7 +1823,7 @@ Room.prototype.getAccountData = function(type) {
|
||||
|
||||
|
||||
/**
|
||||
* Returns wheter the syncing user has permission to send a message in the room
|
||||
* Returns whether the syncing user has permission to send a message in the room
|
||||
* @return {boolean} true if the user should be permitted to send
|
||||
* message events into the room.
|
||||
*/
|
||||
@@ -1831,6 +1832,30 @@ Room.prototype.maySendMessage = function() {
|
||||
this.currentState.maySendEvent('m.room.message', this.myUserId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given user has permissions to issue an invite for this room.
|
||||
* @param {string} userId the ID of the Matrix user to check permissions for
|
||||
* @returns {boolean} true if the user should be permitted to issue invites for this room.
|
||||
*/
|
||||
Room.prototype.canInvite = function(userId) {
|
||||
let canInvite = this.getMyMembership() === "join";
|
||||
const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
|
||||
const me = this.getMember(userId);
|
||||
if (powerLevels && me && powerLevels.invite > me.powerLevel) {
|
||||
canInvite = false;
|
||||
}
|
||||
return canInvite;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
Room.prototype.getJoinRule = function() {
|
||||
return this.currentState.getJoinRule();
|
||||
};
|
||||
|
||||
/**
|
||||
* This is an internal method. Calculates the name of the room from the current
|
||||
* room state.
|
||||
@@ -2048,3 +2073,12 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
*
|
||||
* @param {EventStatus} oldStatus The previous event status.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the logged in user's membership in the room is updated.
|
||||
*
|
||||
* @event module:models/room~Room#"Room.myMembership"
|
||||
* @param {Room} room The room in which the membership has been updated
|
||||
* @param {string} membership The new membership value
|
||||
* @param {string} prevMembership The previous membership value
|
||||
*/
|
||||
|
||||
+16
-1
@@ -15,9 +15,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const DIGITS = "0123456789";
|
||||
|
||||
export function randomString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
}
|
||||
|
||||
export function randomLowercaseString(len: number): string {
|
||||
return randomStringFrom(len, LOWERCASE);
|
||||
}
|
||||
|
||||
export function randomUppercaseString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE);
|
||||
}
|
||||
|
||||
function randomStringFrom(len: number, chars: string): string {
|
||||
let ret = "";
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime) {
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-21
@@ -93,7 +93,7 @@ export function SyncApi(client, opts) {
|
||||
};
|
||||
}
|
||||
this.opts = opts;
|
||||
this._peekRoomId = null;
|
||||
this._peekRoom = null;
|
||||
this._currentSyncRequest = null;
|
||||
this._syncState = null;
|
||||
this._syncStateData = null; // additional data (eg. error object for failed sync)
|
||||
@@ -264,17 +264,18 @@ SyncApi.prototype.syncLeftRooms = function() {
|
||||
* store.
|
||||
*/
|
||||
SyncApi.prototype.peek = function(roomId) {
|
||||
const self = this;
|
||||
if (this._peekRoom && this._peekRoom.roomId === roomId) {
|
||||
return Promise.resolve(this._peekRoom);
|
||||
}
|
||||
|
||||
const client = this.client;
|
||||
this._peekRoomId = roomId;
|
||||
return this.client.roomInitialSync(roomId, 20).then(function(response) {
|
||||
this._peekRoom = this.createRoom(roomId);
|
||||
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
||||
// make sure things are init'd
|
||||
response.messages = response.messages || {};
|
||||
response.messages.chunk = response.messages.chunk || [];
|
||||
response.state = response.state || [];
|
||||
|
||||
const peekRoom = self.createRoom(roomId);
|
||||
|
||||
// FIXME: Mostly duplicated from _processRoomEvents but not entirely
|
||||
// because "state" in this API is at the BEGINNING of the chunk
|
||||
const oldStateEvents = utils.map(
|
||||
@@ -309,28 +310,28 @@ SyncApi.prototype.peek = function(roomId) {
|
||||
// fire off pagination requests in response to the Room.timeline
|
||||
// events.
|
||||
if (response.messages.start) {
|
||||
peekRoom.oldState.paginationToken = response.messages.start;
|
||||
this._peekRoom.oldState.paginationToken = response.messages.start;
|
||||
}
|
||||
|
||||
// set the state of the room to as it was after the timeline executes
|
||||
peekRoom.oldState.setStateEvents(oldStateEvents);
|
||||
peekRoom.currentState.setStateEvents(stateEvents);
|
||||
this._peekRoom.oldState.setStateEvents(oldStateEvents);
|
||||
this._peekRoom.currentState.setStateEvents(stateEvents);
|
||||
|
||||
self._resolveInvites(peekRoom);
|
||||
peekRoom.recalculate();
|
||||
this._resolveInvites(this._peekRoom);
|
||||
this._peekRoom.recalculate();
|
||||
|
||||
// roll backwards to diverge old state. addEventsToTimeline
|
||||
// will overwrite the pagination token, so make sure it overwrites
|
||||
// it with the right thing.
|
||||
peekRoom.addEventsToTimeline(messages.reverse(), true,
|
||||
peekRoom.getLiveTimeline(),
|
||||
this._peekRoom.addEventsToTimeline(messages.reverse(), true,
|
||||
this._peekRoom.getLiveTimeline(),
|
||||
response.messages.start);
|
||||
|
||||
client.store.storeRoom(peekRoom);
|
||||
client.emit("Room", peekRoom);
|
||||
client.store.storeRoom(this._peekRoom);
|
||||
client.emit("Room", this._peekRoom);
|
||||
|
||||
self._peekPoll(peekRoom);
|
||||
return peekRoom;
|
||||
this._peekPoll(this._peekRoom);
|
||||
return this._peekRoom;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -339,16 +340,16 @@ SyncApi.prototype.peek = function(roomId) {
|
||||
* peeked.
|
||||
*/
|
||||
SyncApi.prototype.stopPeeking = function() {
|
||||
this._peekRoomId = null;
|
||||
this._peekRoom = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a peek room poll.
|
||||
* @param {Room} peekRoom
|
||||
* @param {string} token from= token
|
||||
* @param {string?} token from= token
|
||||
*/
|
||||
SyncApi.prototype._peekPoll = function(peekRoom, token) {
|
||||
if (this._peekRoomId !== peekRoom.roomId) {
|
||||
if (this._peekRoom !== peekRoom) {
|
||||
debuglog("Stopped peeking in room %s", peekRoom.roomId);
|
||||
return;
|
||||
}
|
||||
@@ -360,7 +361,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
|
||||
timeout: 30 * 1000,
|
||||
from: token,
|
||||
}, undefined, 50 * 1000).then(function(res) {
|
||||
if (self._peekRoomId !== peekRoom.roomId) {
|
||||
if (self._peekRoom !== peekRoom) {
|
||||
debuglog("Stopped peeking in room %s", peekRoom.roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
+221
-37
@@ -27,6 +27,8 @@ import * as utils from '../utils';
|
||||
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';
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
|
||||
@@ -183,6 +185,21 @@ const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
|
||||
/** The length of time a call can be ringing for. */
|
||||
const CALL_TIMEOUT_MS = 60000;
|
||||
|
||||
/** Retrieves sources from desktopCapturer */
|
||||
export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> {
|
||||
const options: GetSourcesOptions = {
|
||||
thumbnailSize: {
|
||||
height: 176,
|
||||
width: 312,
|
||||
},
|
||||
types: [
|
||||
"screen",
|
||||
"window",
|
||||
],
|
||||
};
|
||||
return window.electron.getDesktopCapturerSources(options);
|
||||
}
|
||||
|
||||
export class CallError extends Error {
|
||||
code : string;
|
||||
|
||||
@@ -194,6 +211,10 @@ export class CallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function genCallID(): string {
|
||||
return Date.now().toString() + randomString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Matrix Call.
|
||||
* @constructor
|
||||
@@ -240,6 +261,7 @@ export class MatrixCall extends EventEmitter {
|
||||
// The party ID of the other side: undefined if we haven't chosen a partner
|
||||
// yet, null if we have but they didn't send a party ID.
|
||||
private opponentPartyId: string;
|
||||
private opponentCaps: CallCapabilities;
|
||||
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
|
||||
|
||||
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
|
||||
@@ -253,6 +275,11 @@ export class MatrixCall extends EventEmitter {
|
||||
private micMuted;
|
||||
private vidMuted;
|
||||
|
||||
// the stats for the call at the point it ended. We can't get these after we
|
||||
// tear the call down, so we just grab a snapshot before we stop the call.
|
||||
// The typescript definitions have this type as 'any' :(
|
||||
private callStatsAtEnd: any[];
|
||||
|
||||
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
|
||||
private makingOffer: boolean;
|
||||
private ignoreOffer: boolean;
|
||||
@@ -264,6 +291,9 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = null;
|
||||
this.forceTURN = opts.forceTURN;
|
||||
this.ourPartyId = this.client.deviceId;
|
||||
// We compare this to null to checks the presence of a party ID:
|
||||
// make sure it's null, not undefined
|
||||
this.opponentPartyId = null;
|
||||
// Array of Objects with urls, username, credential keys
|
||||
this.turnServers = opts.turnServers || [];
|
||||
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
||||
@@ -275,7 +305,7 @@ export class MatrixCall extends EventEmitter {
|
||||
utils.checkObjectHasKeys(server, ["urls"]);
|
||||
}
|
||||
|
||||
this.callId = "c" + new Date().getTime() + Math.random();
|
||||
this.callId = genCallID();
|
||||
this.state = CallState.Fledgling;
|
||||
|
||||
// A queue for candidates waiting to go out.
|
||||
@@ -332,37 +362,80 @@ export class MatrixCall extends EventEmitter {
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
async placeScreenSharingCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
|
||||
async placeScreenSharingCall(
|
||||
remoteVideoElement: HTMLVideoElement,
|
||||
localVideoElement: HTMLVideoElement,
|
||||
selectDesktopCapturerSource: () => Promise<DesktopCapturerSource>,
|
||||
) {
|
||||
logger.debug("placeScreenSharingCall");
|
||||
this.checkForErrorListener();
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
try {
|
||||
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
|
||||
logger.debug("Got screen stream, requesting audio stream...");
|
||||
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
|
||||
this.placeCallWithConstraints(audioConstraints);
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(
|
||||
CallErrorCode.NoUserMedia,
|
||||
"Failed to get screen-sharing stream: ", err,
|
||||
),
|
||||
);
|
||||
|
||||
if (window.electron?.getDesktopCapturerSources) {
|
||||
// We have access to getDesktopCapturerSources()
|
||||
logger.debug("Electron getDesktopCapturerSources() is available...");
|
||||
try {
|
||||
const selectedSource = await selectDesktopCapturerSource();
|
||||
// If no source was selected cancel call
|
||||
if (!selectedSource) return;
|
||||
const getUserMediaOptions: MediaStreamConstraints | DesktopCapturerConstraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: selectedSource.id,
|
||||
},
|
||||
},
|
||||
}
|
||||
this.screenSharingStream = await window.navigator.mediaDevices.getUserMedia(getUserMediaOptions);
|
||||
|
||||
logger.debug("Got screen stream, requesting audio stream...");
|
||||
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
|
||||
this.placeCallWithConstraints(audioConstraints);
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(
|
||||
CallErrorCode.NoUserMedia,
|
||||
"Failed to get screen-sharing stream: ", err,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
/* We do not have access to the Electron desktop capturer,
|
||||
* therefore we can assume we are on the web */
|
||||
logger.debug("Electron desktopCapturer is not available...");
|
||||
try {
|
||||
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
|
||||
logger.debug("Got screen stream, requesting audio stream...");
|
||||
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
|
||||
this.placeCallWithConstraints(audioConstraints);
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(
|
||||
CallErrorCode.NoUserMedia,
|
||||
"Failed to get screen-sharing stream: ", err,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.type = CallType.Video;
|
||||
}
|
||||
|
||||
getOpponentMember() {
|
||||
public getOpponentMember() {
|
||||
return this.opponentMember;
|
||||
}
|
||||
|
||||
public opponentCanBeTransferred() {
|
||||
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the local <code><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
getLocalVideoElement(): HTMLVideoElement {
|
||||
public getLocalVideoElement(): HTMLVideoElement {
|
||||
return this.localVideoElement;
|
||||
}
|
||||
|
||||
@@ -371,7 +444,7 @@ export class MatrixCall extends EventEmitter {
|
||||
* used for playing back video capable streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
getRemoteVideoElement(): HTMLVideoElement {
|
||||
public getRemoteVideoElement(): HTMLVideoElement {
|
||||
return this.remoteVideoElement;
|
||||
}
|
||||
|
||||
@@ -380,7 +453,7 @@ export class MatrixCall extends EventEmitter {
|
||||
* used for playing back audio only streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
getRemoteAudioElement(): HTMLAudioElement {
|
||||
public getRemoteAudioElement(): HTMLAudioElement {
|
||||
return this.remoteAudioElement;
|
||||
}
|
||||
|
||||
@@ -389,7 +462,7 @@ export class MatrixCall extends EventEmitter {
|
||||
* video will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
async setLocalVideoElement(element : HTMLVideoElement) {
|
||||
public async setLocalVideoElement(element : HTMLVideoElement) {
|
||||
this.localVideoElement = element;
|
||||
|
||||
if (element && this.localAVStream && this.type === CallType.Video) {
|
||||
@@ -410,7 +483,7 @@ export class MatrixCall extends EventEmitter {
|
||||
* the first received video-capable stream will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
setRemoteVideoElement(element : HTMLVideoElement) {
|
||||
public setRemoteVideoElement(element : HTMLVideoElement) {
|
||||
if (element === this.remoteVideoElement) return;
|
||||
|
||||
element.autoplay = true;
|
||||
@@ -432,7 +505,7 @@ export class MatrixCall extends EventEmitter {
|
||||
* The audio will *not* be rendered from the remoteVideoElement.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
async setRemoteAudioElement(element: HTMLAudioElement) {
|
||||
public async setRemoteAudioElement(element: HTMLAudioElement) {
|
||||
if (element === this.remoteAudioElement) return;
|
||||
|
||||
this.remoteAudioElement = element;
|
||||
@@ -440,12 +513,36 @@ export class MatrixCall extends EventEmitter {
|
||||
if (this.remoteStream) this.playRemoteAudio();
|
||||
}
|
||||
|
||||
// The typescript definitions have this type as 'any' :(
|
||||
public async getCurrentCallStats(): Promise<any[]> {
|
||||
if (this.callHasEnded()) {
|
||||
return this.callStatsAtEnd;
|
||||
}
|
||||
|
||||
return this.collectCallStats();
|
||||
}
|
||||
|
||||
private async collectCallStats(): Promise<any[]> {
|
||||
// This happens when the call fails before it starts.
|
||||
// For example when we fail to get capture sources
|
||||
if (!this.peerConn) return;
|
||||
|
||||
const statsReport = await this.peerConn.getStats();
|
||||
const stats = [];
|
||||
for (const item of statsReport) {
|
||||
stats.push(item[1]);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure this call from an invite event. Used by MatrixClient.
|
||||
* @param {MatrixEvent} event The m.call.invite event
|
||||
*/
|
||||
async initWithInvite(event: MatrixEvent) {
|
||||
this.msg = event.getContent();
|
||||
this.direction = CallDirection.Inbound;
|
||||
this.peerConn = this.createPeerConnection();
|
||||
try {
|
||||
await this.peerConn.setRemoteDescription(this.msg.offer);
|
||||
@@ -467,9 +564,12 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||
|
||||
this.setState(CallState.Ringing);
|
||||
this.direction = CallDirection.Inbound;
|
||||
this.opponentVersion = this.msg.version;
|
||||
this.opponentPartyId = this.msg.party_id || null;
|
||||
if (this.opponentVersion !== 0) {
|
||||
// ignore party ID in v0 calls: party ID isn't a thing until v1
|
||||
this.opponentPartyId = this.msg.party_id || null;
|
||||
}
|
||||
this.opponentCaps = this.msg.capabilities || {};
|
||||
this.opponentMember = event.sender;
|
||||
|
||||
if (event.getLocalAge()) {
|
||||
@@ -694,6 +794,21 @@ export class MatrixCall extends EventEmitter {
|
||||
return callOnHold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DTMF digit to the other party
|
||||
* @param digit The digit (nb. string - '#' and '*' are dtmf too)
|
||||
*/
|
||||
sendDtmfDigit(digit: string) {
|
||||
for (const sender of this.peerConn.getSenders()) {
|
||||
if (sender.track.kind === 'audio' && sender.dtmf) {
|
||||
sender.dtmf.insertDTMF(digit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to find a track to send DTMF on");
|
||||
}
|
||||
|
||||
private updateMuteStatus() {
|
||||
if (!this.localAVStream) {
|
||||
return;
|
||||
@@ -764,7 +879,7 @@ export class MatrixCall extends EventEmitter {
|
||||
this.peerConn.addTrack(audioTrack, stream);
|
||||
}
|
||||
for (const videoTrack of (this.screenSharingStream || stream).getVideoTracks()) {
|
||||
logger.info("Adding audio track with id " + videoTrack.id);
|
||||
logger.info("Adding video track with id " + videoTrack.id);
|
||||
this.peerConn.addTrack(videoTrack, stream);
|
||||
}
|
||||
|
||||
@@ -779,7 +894,14 @@ export class MatrixCall extends EventEmitter {
|
||||
// required to still be sent for backwards compat
|
||||
type: this.peerConn.localDescription.type,
|
||||
},
|
||||
};
|
||||
} as MCallAnswer;
|
||||
|
||||
if (this.client._supportsCallTransfer) {
|
||||
answerContent.capabilities = {
|
||||
'm.call.transferee': true,
|
||||
}
|
||||
}
|
||||
|
||||
// We have just taken the local description from the peerconnection which will
|
||||
// contain all the local candidates added so far, so we can discard any candidates
|
||||
// we had queued up because they'll be in the answer.
|
||||
@@ -950,7 +1072,7 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opponentPartyId !== undefined) {
|
||||
if (this.opponentPartyId !== null) {
|
||||
logger.info(
|
||||
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
|
||||
`we already have an answer/reject from ${this.opponentPartyId}`,
|
||||
@@ -959,7 +1081,10 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
this.opponentVersion = event.getContent().version;
|
||||
this.opponentPartyId = event.getContent().party_id || null;
|
||||
if (this.opponentVersion !== 0) {
|
||||
this.opponentPartyId = event.getContent().party_id || null;
|
||||
}
|
||||
this.opponentCaps = event.getContent().capabilities || {};
|
||||
this.opponentMember = event.sender;
|
||||
|
||||
this.setState(CallState.Connecting);
|
||||
@@ -1044,8 +1169,21 @@ export class MatrixCall extends EventEmitter {
|
||||
await this.peerConn.setRemoteDescription(description);
|
||||
|
||||
if (description.type === 'offer') {
|
||||
// First we sent the direction of the tranciever to what we'd like it to be,
|
||||
// irresepective of whether the other side has us on hold - so just whether we
|
||||
// want the call to be on hold or not. This is necessary because in a few lines,
|
||||
// we'll adjust the direction and unless we do this too, we'll never come off hold.
|
||||
for (const tranceiver of this.peerConn.getTransceivers()) {
|
||||
tranceiver.direction = this.isRemoteOnHold() ? 'inactive' : 'sendrecv';
|
||||
}
|
||||
const localDescription = await this.peerConn.createAnswer();
|
||||
await this.peerConn.setLocalDescription(localDescription);
|
||||
// Now we've got our answer, set the direction to the outcome of the negotiation.
|
||||
// We need to do this otherwise Firefox will notice that the direction is not the
|
||||
// currentDirection and try to negotiate itself off hold again.
|
||||
for (const tranceiver of this.peerConn.getTransceivers()) {
|
||||
tranceiver.direction = tranceiver.currentDirection;
|
||||
}
|
||||
|
||||
this.sendVoipEvent(EventType.CallNegotiate, {
|
||||
description: this.peerConn.localDescription,
|
||||
@@ -1096,13 +1234,24 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
const keyName = this.state === CallState.CreateOffer ? 'offer' : 'description';
|
||||
const eventType = this.state === CallState.CreateOffer ? EventType.CallInvite : EventType.CallNegotiate;
|
||||
|
||||
const content = {
|
||||
[keyName]: this.peerConn.localDescription,
|
||||
lifetime: CALL_TIMEOUT_MS,
|
||||
};
|
||||
} as MCallOfferNegotiate;
|
||||
|
||||
// clunky because TypeScript can't folow the types through if we use an expression as the key
|
||||
if (this.state === CallState.CreateOffer) {
|
||||
content.offer = this.peerConn.localDescription;
|
||||
} else {
|
||||
content.description = this.peerConn.localDescription;
|
||||
}
|
||||
|
||||
if (this.client._supportsCallTransfer) {
|
||||
content.capabilities = {
|
||||
'm.call.transferee': true,
|
||||
}
|
||||
}
|
||||
|
||||
// Get rid of any candidates waiting to be sent: they'll be included in the local
|
||||
// description we just got and will send in the offer.
|
||||
@@ -1178,7 +1327,7 @@ export class MatrixCall extends EventEmitter {
|
||||
return; // because ICE can still complete as we're ending the call
|
||||
}
|
||||
logger.debug(
|
||||
"ICE connection state changed to: " + this.peerConn.iceConnectionState,
|
||||
"Call ID " + this.callId + ": ICE connection state changed to: " + this.peerConn.iceConnectionState,
|
||||
);
|
||||
// ideally we'd consider the call to be connected when we get media but
|
||||
// chrome doesn't implement any of the 'onstarted' events yet
|
||||
@@ -1296,11 +1445,11 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
onHangupReceived = (msg) => {
|
||||
logger.debug("Hangup received");
|
||||
logger.debug("Hangup received for call ID " + this.callId);
|
||||
|
||||
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
|
||||
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
|
||||
if (this.partyIdMatches(msg) || this.opponentPartyId === undefined || this.state === CallState.Ringing) {
|
||||
if (this.partyIdMatches(msg) || this.state === CallState.Ringing) {
|
||||
// default reason is user_hangup
|
||||
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
|
||||
} else {
|
||||
@@ -1309,12 +1458,21 @@ export class MatrixCall extends EventEmitter {
|
||||
};
|
||||
|
||||
onRejectReceived = (msg) => {
|
||||
logger.debug("Reject received");
|
||||
logger.debug("Reject received for call ID " + this.callId);
|
||||
|
||||
// No need to check party_id for reject because if we'd received either
|
||||
// an answer or reject, we wouldn't be in state InviteSent
|
||||
|
||||
if (this.state === CallState.InviteSent) {
|
||||
const shouldTerminate = (
|
||||
// reject events also end the call if it's ringing: it's another of
|
||||
// our devices rejecting the call.
|
||||
([CallState.InviteSent, CallState.Ringing].includes(this.state)) ||
|
||||
// also if we're in the init state and it's an inbound call, since
|
||||
// this means we just haven't entered the ringing state yet
|
||||
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound
|
||||
);
|
||||
|
||||
if (shouldTerminate) {
|
||||
this.terminate(CallParty.Remote, CallErrorCode.UserHangup, true);
|
||||
} else {
|
||||
logger.debug(`Call is in state: ${this.state}: ignoring reject`);
|
||||
@@ -1322,7 +1480,7 @@ export class MatrixCall extends EventEmitter {
|
||||
};
|
||||
|
||||
onAnsweredElsewhere = (msg) => {
|
||||
logger.debug("Answered elsewhere");
|
||||
logger.debug("Call ID " + this.callId + " answered elsewhere");
|
||||
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
|
||||
};
|
||||
|
||||
@@ -1370,9 +1528,33 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async transfer(targetUserId: string, targetRoomId?: 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);
|
||||
|
||||
const replacementId = genCallID();
|
||||
|
||||
const body = {
|
||||
replacement_id: genCallID(),
|
||||
target_user: {
|
||||
id: targetUserId,
|
||||
display_name: profileInfo.display_name,
|
||||
avatar_url: profileInfo.avatar_url,
|
||||
},
|
||||
create_call: replacementId,
|
||||
} as MCallReplacesEvent;
|
||||
|
||||
if (targetRoomId) body.target_room = targetRoomId;
|
||||
|
||||
return this.sendVoipEvent(EventType.CallReplaces, body);
|
||||
}
|
||||
|
||||
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
this.callStatsAtEnd = await this.collectCallStats();
|
||||
|
||||
if (this.inviteTimeout) {
|
||||
clearTimeout(this.inviteTimeout);
|
||||
this.inviteTimeout = null;
|
||||
@@ -1504,6 +1686,7 @@ export class MatrixCall extends EventEmitter {
|
||||
const pc = new window.RTCPeerConnection({
|
||||
iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
|
||||
iceServers: this.turnServers,
|
||||
iceCandidatePoolSize: this.client._iceCandidatePoolSize,
|
||||
});
|
||||
|
||||
// 'connectionstatechange' would be better, but firefox doesn't implement that.
|
||||
@@ -1596,7 +1779,8 @@ export function setVideoInput(deviceId: string) { videoInput = deviceId; }
|
||||
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts) {
|
||||
// typeof prevents Node from erroring on an undefined reference
|
||||
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
|
||||
logger.info("No window or document object: WebRTC is not supported in this environment");
|
||||
// NB. We don't log here as apps try to create a call object as a test for
|
||||
// whether calls are supported, so we shouldn't fill the logs up.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// allow camelcase as these are events type that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
interface CallOfferAnswer {
|
||||
type: string;
|
||||
sdp: string;
|
||||
}
|
||||
|
||||
export interface CallCapabilities {
|
||||
'm.call.transferee': boolean;
|
||||
}
|
||||
|
||||
export interface MCallAnswer {
|
||||
answer: CallOfferAnswer;
|
||||
capabilities: CallCapabilities;
|
||||
}
|
||||
|
||||
export interface MCallOfferNegotiate {
|
||||
offer: CallOfferAnswer;
|
||||
description: CallOfferAnswer;
|
||||
lifetime: number;
|
||||
capabilities: CallCapabilities;
|
||||
}
|
||||
|
||||
export interface MCallReplacesTarget {
|
||||
id: string;
|
||||
display_name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export interface MCallReplacesEvent {
|
||||
replacement_id: string;
|
||||
target_user: MCallReplacesTarget;
|
||||
create_call: string;
|
||||
target_room: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
Reference in New Issue
Block a user