Compare commits
140 Commits
v14.0.0-rc.1
...
v15.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a0bdb0f98 | |||
| e870bebd3d | |||
| 22a98b3e4f | |||
| 934a0e8943 | |||
| aeec4aa4a8 | |||
| 169b6b5572 | |||
| 6244d77d44 | |||
| f4839a3b4f | |||
| 4b21b67a45 | |||
| 799a83a115 | |||
| f3ebb2a314 | |||
| c0fbed914e | |||
| dd8c157bb9 | |||
| 8bd2d640f2 | |||
| 80aaa6c32b | |||
| 4e9fe8f53f | |||
| f926a2f777 | |||
| f8097221e6 | |||
| 95e7a76ba9 | |||
| 72bc13eb39 | |||
| 2206b80e65 | |||
| 39a8399977 | |||
| b7247fcedc | |||
| e4494707fa | |||
| 45b7cab203 | |||
| 4dfc3b9dbb | |||
| 15d05cd92f | |||
| ba1b1cc6fa | |||
| b716cde9b5 | |||
| 46a0645865 | |||
| 2b30f45f60 | |||
| 87b920698f | |||
| 4f4811e1d9 | |||
| db9936e07c | |||
| 7dcee2eb40 | |||
| 6b34ea6fe5 | |||
| b33b01df0f | |||
| 1c895432ed | |||
| ddd6a05198 | |||
| 0ccde7807f | |||
| 9f97992196 | |||
| 67b8da719f | |||
| 04fad564f7 | |||
| aee698c285 | |||
| 55b489b17a | |||
| c5d188c30a | |||
| 6fff3d6db5 | |||
| 8043d83921 | |||
| b2d83c1f80 | |||
| 1b91cd7037 | |||
| 0baf926c84 | |||
| 763e25e031 | |||
| 861023b3f6 | |||
| 1f5a83994b | |||
| af523522de | |||
| c3a266b3e7 | |||
| f41d815aa6 | |||
| ad8a93dde8 | |||
| b07d44a6c0 | |||
| 1dc899ba6e | |||
| 0b1e3edaff | |||
| 9fb4fed044 | |||
| 730b0b121d | |||
| 9d9d9e2cfa | |||
| 43bc09f392 | |||
| 195498e9db | |||
| 50332c4999 | |||
| 9e46646b16 | |||
| a74a03e414 | |||
| fe8b9eca8e | |||
| a7cc1ae630 | |||
| 14e008bbad | |||
| 6a12c265cf | |||
| c35cb57a79 | |||
| 51c776f51e | |||
| 7ce0eb26d4 | |||
| f0091fa811 | |||
| 25c88bc986 | |||
| ab06f57965 | |||
| c46209a159 | |||
| 0c47e27f9f | |||
| 0385f265e8 | |||
| 59b3960a42 | |||
| d5ab4ba2ef | |||
| f59b81f46b | |||
| 4c39ce8c96 | |||
| 94ff0ea4cd | |||
| 526758a07f | |||
| 9686fba81e | |||
| bbc547fd7f | |||
| 7d5dbeaf2d | |||
| 8c19cf45e2 | |||
| c9277de8ee | |||
| b5baca51a6 | |||
| fc800821f9 | |||
| efbc46937f | |||
| 9ceeb2d220 | |||
| 7b89862ed0 | |||
| 6804e4290a | |||
| 0a3d820fda | |||
| e2a9c95ea8 | |||
| 3aefc9f02e | |||
| 0042cb5292 | |||
| b288c97372 | |||
| a0de1740a4 | |||
| 8e2e3018d8 | |||
| 60e93024e7 | |||
| d89aa365f9 | |||
| 4241263f2c | |||
| b009f9c21b | |||
| 04ccec6de3 | |||
| e7b41fadd0 | |||
| 2dc1acef2e | |||
| b977b629f4 | |||
| 6bbd493e20 | |||
| 38ad435af5 | |||
| 8f245af317 | |||
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 7559a10526 | |||
| 68dbe959bb | |||
| 74a875aa23 | |||
| 4b679c5056 | |||
| cb8a260b9c | |||
| 7d7975b8bb | |||
| f12513da96 | |||
| 785a3e3381 | |||
| f5fcc20840 | |||
| 17e20c17b3 | |||
| 39fd350ffe | |||
| b4d100baff | |||
| 29bc83f4a7 | |||
| 13c8774d45 | |||
| c9212e770d | |||
| 08aa7803e3 | |||
| 770858c255 | |||
| c73bb7d408 | |||
| c63abe9988 | |||
| 482e3c4cb7 | |||
| 3c97d17770 |
+148
@@ -1,3 +1,151 @@
|
||||
Changes in [15.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
|
||||
==================================================================================================
|
||||
|
||||
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
|
||||
|
||||
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1) (2021-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1-rc.1) (2021-11-17)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0) (2021-11-08)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0-rc.1) (2021-11-02)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
|
||||
=============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
|
||||
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
|
||||
=============================================================================================================
|
||||
|
||||
|
||||
+9
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "14.0.0-rc.1",
|
||||
"version": "15.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -15,7 +15,7 @@
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 7 src spec",
|
||||
"lint:js": "eslint --max-warnings 4 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
@@ -55,6 +55,7 @@
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"loglevel": "^1.7.1",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
@@ -74,14 +75,14 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"allchange": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
@@ -89,7 +90,7 @@
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#48ec1e6af2cfb8310b9a6e23edf2dc7a26ddd580",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
@@ -99,7 +100,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export class MockBlob {
|
||||
private contents: number[] = [];
|
||||
|
||||
public constructor(private parts: ArrayLike<number>[]) {
|
||||
parts.forEach(p => Array.from(p).forEach(e => this.contents.push(e)));
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.contents.length;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { WebStorageSessionStore } from "../src/store/session/webstorage";
|
||||
|
||||
@@ -17,9 +17,10 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
|
||||
+15
-9
@@ -4,13 +4,13 @@ import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let client: TestClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
@@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() {
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId);
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
@@ -50,17 +50,23 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
|
||||
const p1 = client.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
// we expect the first message to fail
|
||||
throw new Error('Message 1 unexpectedly sent successfully');
|
||||
}, (e) => {
|
||||
}, () => {
|
||||
// this is expected
|
||||
});
|
||||
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client.sendMessage(roomId, "m2");
|
||||
client.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
@@ -72,7 +78,7 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
@@ -88,7 +94,7 @@ describe("MatrixClient retrying", function() {
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import * as testUtils from "../test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as sdk from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import '../olm-loader';
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
@@ -8,7 +10,6 @@ import { MatrixEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { HttpResponse, setHttpResponses } from '../../test-utils';
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
|
||||
@@ -23,7 +23,6 @@ import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
try {
|
||||
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
|
||||
@@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import { logger } from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
|
||||
@@ -244,8 +244,7 @@ describe("MSC3089Branch", () => {
|
||||
|
||||
it('should create new versions of itself', async () => {
|
||||
const canaryName = "canary";
|
||||
const fileContents = "contents go here";
|
||||
const canaryContents = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
const canaryContents = "contents go here";
|
||||
const canaryFile = {} as IEncryptedFile;
|
||||
const canaryAddl = { canary: true };
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
TreePermissions,
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MockBlob } from "../../MockBlob";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
@@ -887,12 +886,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
@@ -930,7 +925,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { metadata: true });
|
||||
@@ -951,12 +946,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
@@ -997,7 +988,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { "m.new_content": true });
|
||||
@@ -1027,7 +1018,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should return falsy for unknown files', () => {
|
||||
const fileEventId = "$file";
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return null;
|
||||
|
||||
+15
-66
@@ -26,6 +26,15 @@ describe("utils", function() {
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle boolean and numeric values", function() {
|
||||
const params = {
|
||||
string: "foobar",
|
||||
number: 12345,
|
||||
boolean: false,
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
@@ -111,10 +120,10 @@ describe("utils", function() {
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
isTrue: function(x: any) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
isFalse: function(x: any) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
@@ -176,10 +185,10 @@ describe("utils", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
const func = function() {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
const func2 = function() {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
@@ -189,66 +198,6 @@ describe("utils", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Fix type
|
||||
const target: any = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
@@ -273,7 +222,7 @@ describe("utils", function() {
|
||||
it('should retry', async () => {
|
||||
let count = 0;
|
||||
const val = {};
|
||||
const fn = (attempt) => {
|
||||
const fn = (attempt: any) => {
|
||||
count++;
|
||||
|
||||
// If this expectation fails then it can appear as a Jest Timeout due to
|
||||
@@ -480,7 +429,7 @@ describe("utils", function() {
|
||||
},
|
||||
[72]: "test",
|
||||
};
|
||||
const output = [
|
||||
const output: any = [
|
||||
["72", "test"],
|
||||
["a", 42],
|
||||
["b", [
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
||||
import { RoomMember } from "../../../src";
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
@@ -85,7 +86,7 @@ class MockRTCPeerConnection {
|
||||
|
||||
class MockMediaStream {
|
||||
constructor(
|
||||
public id,
|
||||
public id: string,
|
||||
) {}
|
||||
|
||||
getTracks() { return []; }
|
||||
@@ -94,6 +95,17 @@ class MockMediaStream {
|
||||
addEventListener() {}
|
||||
}
|
||||
|
||||
class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
) {}
|
||||
}
|
||||
|
||||
class MockMediaHandler {
|
||||
getUserMediaStream() { return new MockMediaStream("mock_stream_from_media_handler"); }
|
||||
stopUserMediaStream() {}
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
let call;
|
||||
@@ -110,6 +122,8 @@ describe('Call', function() {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
// @ts-ignore Mock
|
||||
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -129,6 +143,8 @@ describe('Call', function() {
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.client.mediaHandler = new MockMediaHandler;
|
||||
client.client.getMediaHandler = () => client.client.mediaHandler;
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
@@ -347,7 +363,7 @@ describe('Call', function() {
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" };
|
||||
return { userId: "@bob:bar.uk" } as RoomMember;
|
||||
};
|
||||
|
||||
await call.onAnswerReceived({
|
||||
@@ -368,4 +384,16 @@ describe('Call', function() {
|
||||
call.setScreensharingEnabled(true);
|
||||
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fallback to answering with no video", async () => {
|
||||
await client.httpBackend.flush();
|
||||
|
||||
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
|
||||
|
||||
await call.answer(true, true);
|
||||
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,12 +147,12 @@ export interface IPusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
format?: string; // TODO: Types
|
||||
format?: string;
|
||||
url?: string; // TODO: Required if kind==http
|
||||
brand?: string; // TODO: For email notifications only?
|
||||
brand?: string; // TODO: For email notifications only? Unspecced field
|
||||
};
|
||||
device_display_name: string;
|
||||
kind: string; // TODO: Types
|
||||
kind: "http" | string;
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
|
||||
+9
-5
@@ -92,6 +92,14 @@ export enum EventType {
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
/**
|
||||
* Note, "io.element.thread" is hardcoded
|
||||
* Should be replaced with "m.thread" once MSC3440 lands
|
||||
* Can not use `UnstableValue` as TypeScript does not
|
||||
* allow computed values in enums
|
||||
* https://github.com/microsoft/TypeScript/issues/27976
|
||||
*/
|
||||
Thread = "io.element.thread",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
@@ -103,6 +111,7 @@ export enum MsgType {
|
||||
Audio = "m.audio",
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "type";
|
||||
@@ -168,11 +177,6 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
|
||||
"m.in_thread",
|
||||
"io.element.in_thread",
|
||||
);
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
+15
-1
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Callback } from "../client";
|
||||
import { IContent } from "../models/event";
|
||||
import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
@@ -139,4 +139,18 @@ export interface IBindThreePidBody {
|
||||
id_access_token: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
original_event: IEvent;
|
||||
chunk: IEvent[];
|
||||
next_batch?: string;
|
||||
prev_batch?: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export class ReEmitter {
|
||||
// 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) => {
|
||||
const forSource = (...args: any[]) => {
|
||||
// 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
|
||||
|
||||
@@ -17,9 +17,10 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
||||
import { logger } from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
|
||||
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import request from "browser-request";
|
||||
import queryString from "qs";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
|
||||
+497
-114
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -53,13 +53,13 @@ export function getHttpUriForMxc(
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params = {};
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (width) {
|
||||
params["width"] = Math.round(width);
|
||||
params["width"] = Math.round(width).toString();
|
||||
}
|
||||
if (height) {
|
||||
params["height"] = Math.round(height);
|
||||
params["height"] = Math.round(height).toString();
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params["method"] = resizeMethod;
|
||||
|
||||
@@ -20,12 +20,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
@@ -304,7 +304,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
|
||||
const privateKeys: Record<string, Uint8Array> = {};
|
||||
const keys: Record<string, any> = {}; // TODO types
|
||||
const keys: Record<string, ICrossSigningKey> = {};
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
|
||||
@@ -265,8 +265,8 @@ export class DeviceList extends EventEmitter {
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
|
||||
const usersToDownload = [];
|
||||
const promises = [];
|
||||
const usersToDownload: string[] = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
userIds.forEach((u) => {
|
||||
const trackingStatus = this.deviceTrackingStatus[u];
|
||||
@@ -633,7 +633,7 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
const finished = (success: boolean): void => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
|
||||
users.forEach((u) => {
|
||||
this.dirty = true;
|
||||
|
||||
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { PREFIX_UNSTABLE } from "../http-api";
|
||||
|
||||
+16
-3
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import * as algorithms from './algorithms';
|
||||
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
||||
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||
import { IMegolmSessionData } from "./index";
|
||||
|
||||
@@ -542,13 +543,25 @@ export class OlmDevice {
|
||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.getAccount(txn, (account: Account) => {
|
||||
result = JSON.parse(account.fallback_key());
|
||||
result = JSON.parse(account.unpublished_fallback_key());
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async forgetOldFallbackKey(): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.getAccount(txn, (account: Account) => {
|
||||
account.forget_old_fallback_key();
|
||||
this.storeAccount(txn, account);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new outbound session
|
||||
*
|
||||
|
||||
+28
-18
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'stream';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
@@ -21,7 +23,6 @@ import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { EventEmitter } from 'stream';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
@@ -37,7 +38,7 @@ export interface ISecretRequest {
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
|
||||
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||
}
|
||||
@@ -54,6 +55,13 @@ interface IDecryptors {
|
||||
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
|
||||
}
|
||||
|
||||
interface ISecretInfo {
|
||||
encrypted: {
|
||||
// eslint-disable-next-line camelcase
|
||||
key_id: IEncryptedPayload;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
@@ -76,7 +84,7 @@ export class SecretStorage {
|
||||
) {}
|
||||
|
||||
public async getDefaultKeyId(): Promise<string> {
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
|
||||
'm.secret_storage.default_key',
|
||||
);
|
||||
if (!defaultKey) return null;
|
||||
@@ -149,7 +157,7 @@ export class SecretStorage {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (
|
||||
await this.accountDataAdapter.getAccountDataFromServer(
|
||||
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
)
|
||||
);
|
||||
@@ -182,9 +190,9 @@ export class SecretStorage {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
) as ISecretStorageKeyInfo;
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
@@ -230,7 +238,7 @@ export class SecretStorage {
|
||||
* or null/undefined to use the default key.
|
||||
*/
|
||||
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
|
||||
const encrypted = {};
|
||||
const encrypted: Record<string, IEncryptedPayload> = {};
|
||||
|
||||
if (!keys) {
|
||||
const defaultKeyId = await this.getDefaultKeyId();
|
||||
@@ -246,9 +254,9 @@ export class SecretStorage {
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
) as ISecretStorageKeyInfo;
|
||||
);
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
@@ -277,7 +285,7 @@ export class SecretStorage {
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
public async get(name: string): Promise<string> {
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -286,11 +294,13 @@ export class SecretStorage {
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys = {};
|
||||
const keys: Record<string, ISecretStorageKeyInfo> = {};
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
const keyInfo = (
|
||||
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
)
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
// only use keys we understand the encryption algorithm of
|
||||
@@ -306,7 +316,7 @@ export class SecretStorage {
|
||||
`the keys it is encrypted with are for a supported algorithm`);
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let keyId: string;
|
||||
let decryption;
|
||||
try {
|
||||
// fetch private key from app
|
||||
@@ -337,7 +347,7 @@ export class SecretStorage {
|
||||
*/
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
// check if secret exists
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
return null;
|
||||
@@ -350,7 +360,7 @@ export class SecretStorage {
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) continue;
|
||||
@@ -375,8 +385,8 @@ export class SecretStorage {
|
||||
public request(name: string, devices: string[]): ISecretRequest {
|
||||
const requestId = this.baseApis.makeTxnId();
|
||||
|
||||
let resolve: (string) => void;
|
||||
let reject: (Error) => void;
|
||||
let resolve: (s: string) => void;
|
||||
let reject: (e: Error) => void;
|
||||
const promise = new Promise<string>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BinaryLike } from "crypto";
|
||||
|
||||
import { getCrypto } from '../utils';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
*/
|
||||
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
|
||||
|
||||
interface IParams {
|
||||
export interface IParams {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
crypto: Crypto;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
IParams,
|
||||
registerAlgorithm,
|
||||
UnknownDeviceError,
|
||||
} from "./base";
|
||||
@@ -99,6 +100,12 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
|
||||
interface IEncryptedContent {
|
||||
algorithm: string;
|
||||
sender_key: string;
|
||||
ciphertext: Record<string, string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
@@ -238,7 +245,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
startTime: number;
|
||||
};
|
||||
|
||||
constructor(params) {
|
||||
constructor(params: IParams) {
|
||||
super(params);
|
||||
|
||||
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
|
||||
@@ -263,7 +270,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
blocked: IBlockedMap,
|
||||
singleOlmCreationPhase = false,
|
||||
): Promise<OutboundSessionInfo> {
|
||||
let session;
|
||||
let session: OutboundSessionInfo;
|
||||
|
||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||
// a new one. Also shares the key with any (new) devices in the room.
|
||||
@@ -302,7 +309,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
const shareMap = {};
|
||||
const shareMap: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
|
||||
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
|
||||
@@ -341,13 +348,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
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}`);
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
|
||||
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 = [];
|
||||
logger.debug(
|
||||
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
|
||||
devicesWithoutSession,
|
||||
);
|
||||
const errorDevices: IOlmDevice[] = [];
|
||||
|
||||
// meanwhile, establish olm sessions for devices that we don't
|
||||
// already have a session for, and share keys with them. If
|
||||
@@ -355,7 +365,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// shorter timeout when fetching one-time keys for the first
|
||||
// phase.
|
||||
const start = Date.now();
|
||||
const failedServers = [];
|
||||
const failedServers: string[] = [];
|
||||
await this.shareKeyWithDevices(
|
||||
session, key, payload, devicesWithoutSession, errorDevices,
|
||||
singleOlmCreationPhase ? 10000 : 2000, failedServers,
|
||||
@@ -371,7 +381,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// do this in the background and don't block anything else while we
|
||||
// do this. We only need to retry users from servers that didn't
|
||||
// respond the first time.
|
||||
const retryDevices = {};
|
||||
const retryDevices: Record<string, DeviceInfo[]> = {};
|
||||
const failedServerMap = new Set;
|
||||
for (const server of failedServers) {
|
||||
failedServerMap.add(server);
|
||||
@@ -403,8 +413,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
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
|
||||
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
|
||||
Object.entries(blocked));
|
||||
|
||||
// also, notify newly blocked devices that they're blocked
|
||||
logger.debug(`Notifying newly blocked devices in ${this.roomId}`);
|
||||
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
|
||||
let blockedCount = 0;
|
||||
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
|
||||
@@ -421,7 +434,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
|
||||
await this.notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(`Notified ${blockedCount} blocked devices in ${this.roomId}`);
|
||||
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
|
||||
})(),
|
||||
]);
|
||||
};
|
||||
@@ -578,12 +591,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
userDeviceMap: IOlmDevice[],
|
||||
payload: IPayload,
|
||||
): Promise<void> {
|
||||
const contentMap = {};
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
|
||||
|
||||
const promises = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
for (let i = 0; i < userDeviceMap.length; i++) {
|
||||
const encryptedContent = {
|
||||
const encryptedContent: IEncryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
@@ -673,7 +686,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
userDeviceMap: IOlmDevice<IBlockedDevice>[],
|
||||
payload: IPayload,
|
||||
): Promise<void> {
|
||||
const contentMap = {};
|
||||
const contentMap: Record<string, Record<string, IPayload>> = {};
|
||||
|
||||
for (const val of userDeviceMap) {
|
||||
const userId = val.userId;
|
||||
@@ -697,7 +710,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
|
||||
|
||||
// store that we successfully uploaded the keys of the current slice
|
||||
// record the fact that we notified these blocked devices
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
session.markNotifiedBlockedDevice(userId, deviceId);
|
||||
@@ -846,9 +859,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
|
||||
|
||||
logger.debug(`Sharing keys with Olm sessions in ${this.roomId}`);
|
||||
logger.debug(`Sharing keys with newly created Olm sessions in ${this.roomId}`);
|
||||
await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
|
||||
logger.debug(`Shared keys with Olm sessions in ${this.roomId}`);
|
||||
logger.debug(`Shared keys with newly created Olm sessions in ${this.roomId}`);
|
||||
}
|
||||
|
||||
private async shareKeyWithOlmSessions(
|
||||
@@ -864,7 +877,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
`megolm keys for ${session.sessionId} ` +
|
||||
`in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
|
||||
try {
|
||||
logger.debug(`Sharing ${taskDetail}`);
|
||||
logger.debug(`Sharing ${taskDetail}`,
|
||||
userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`));
|
||||
await this.encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
@@ -906,14 +920,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
);
|
||||
}
|
||||
|
||||
const filteredFailedDevices =
|
||||
await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
|
||||
const unnotifiedFailedDevices =
|
||||
await this.olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this.roomId}`,
|
||||
`Need to notify ${unnotifiedFailedDevices.length} failed devices ` +
|
||||
`which haven't been notified before in ${this.roomId}`,
|
||||
);
|
||||
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
|
||||
for (const { userId, deviceInfo } of filteredFailedDevices) {
|
||||
for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
// we use a similar format to what
|
||||
// olmlib.ensureOlmSessionsForDevices returns, so that
|
||||
@@ -930,7 +946,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// send the notifications
|
||||
await this.notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(
|
||||
`Notified ${filteredFailedDevices.length} devices we failed to ` +
|
||||
`Notified ${unnotifiedFailedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this.roomId}`,
|
||||
);
|
||||
}
|
||||
@@ -1096,7 +1112,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* devices we should shared the session with.
|
||||
*/
|
||||
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
|
||||
const unknownDevices = {};
|
||||
const unknownDevices: Record<string, Record<string, DeviceInfo>> = {};
|
||||
|
||||
Object.keys(devicesInRoom).forEach((userId)=>{
|
||||
Object.keys(devicesInRoom[userId]).forEach((deviceId)=>{
|
||||
@@ -1295,8 +1311,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
content.sender_key, event.getTs() - 120000,
|
||||
);
|
||||
if (problem) {
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|
||||
|| PROBLEM_DESCRIPTIONS.unknown;
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
|
||||
if (problem.fixed) {
|
||||
problemDescription +=
|
||||
" Trying to create a new secure channel and re-requesting the keys.";
|
||||
@@ -1390,14 +1405,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
|
||||
const pendingEvents = senderPendingEvents?.get(sessionId);
|
||||
if (!pendingEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingEvents.delete(event);
|
||||
if (pendingEvents.size === 0) {
|
||||
senderPendingEvents.delete(senderKey);
|
||||
senderPendingEvents.delete(sessionId);
|
||||
}
|
||||
if (senderPendingEvents.size === 0) {
|
||||
delete this.pendingEvents[senderKey];
|
||||
@@ -1751,7 +1766,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
}));
|
||||
|
||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
||||
return !((this.pendingEvents[senderKey] || {})[sessionId]);
|
||||
return !this.pendingEvents[senderKey]?.has(sessionId);
|
||||
}
|
||||
|
||||
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
||||
@@ -1785,12 +1800,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
|
||||
|
||||
const promises = [];
|
||||
const contentMap = {};
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
contentMap[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent = {
|
||||
const encryptedContent: IEncryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
|
||||
@@ -282,7 +282,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors = {};
|
||||
const decryptionErrors: Record<string, string> = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
try {
|
||||
|
||||
+6
-12
@@ -58,14 +58,7 @@ export interface IEncryptedEventInfo {
|
||||
}
|
||||
|
||||
export interface IRecoveryKey {
|
||||
keyInfo?: {
|
||||
pubkey: string;
|
||||
passphrase?: {
|
||||
algorithm: string;
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
};
|
||||
keyInfo?: IAddSecretStorageKeyOpts;
|
||||
privateKey: Uint8Array;
|
||||
encodedPrivateKey?: string;
|
||||
}
|
||||
@@ -125,12 +118,13 @@ export interface IPassphraseInfo {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
bits: number;
|
||||
bits?: number;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
name: string;
|
||||
passphrase: IPassphraseInfo;
|
||||
pubkey: string;
|
||||
passphrase?: IPassphraseInfo;
|
||||
name?: string;
|
||||
key: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -142,7 +136,7 @@ export interface IImportOpts {
|
||||
}
|
||||
|
||||
export interface IImportRoomKeysOpts {
|
||||
progressCallback: (stage: IImportOpts) => void;
|
||||
progressCallback?: (stage: IImportOpts) => void;
|
||||
untrusted?: boolean;
|
||||
source?: string; // TODO: Enum
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import { logger } from '../logger';
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { Crypto } from "./index";
|
||||
|
||||
+81
-38
@@ -40,7 +40,7 @@ import {
|
||||
ISecretRequest,
|
||||
SecretStorageKeyObject,
|
||||
} from './SecretStorage';
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import { IAddSecretStorageKeyOpts, ICreateSecretStorageOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||
@@ -58,8 +58,8 @@ import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
|
||||
import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
@@ -108,17 +108,6 @@ export interface IBootstrapCrossSigningOpts {
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => {}): Promise<void>;
|
||||
}
|
||||
|
||||
interface IBootstrapSecretStorageOpts {
|
||||
keyBackupInfo?: any; // TODO types
|
||||
setupNewKeyBackup?: boolean;
|
||||
setupNewSecretStorage?: boolean;
|
||||
createSecretStorageKey?(): Promise<{
|
||||
keyInfo?: any; // TODO types
|
||||
privateKey?: Uint8Array;
|
||||
}>;
|
||||
getKeyBackupPassphrase?(): Promise<Uint8Array | null>;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomKey {
|
||||
room_id: string;
|
||||
@@ -183,10 +172,10 @@ interface ISignableObject {
|
||||
}
|
||||
|
||||
export interface IEventDecryptionResult {
|
||||
clearEvent: object;
|
||||
clearEvent: IClearEvent;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
untrusted?: boolean;
|
||||
}
|
||||
|
||||
@@ -259,6 +248,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private oneTimeKeyCount: number;
|
||||
private needsNewFallback: boolean;
|
||||
private fallbackCleanup?: number; // setTimeout ID
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
@@ -762,12 +752,12 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
// TODO this does not resolve with what it says it does
|
||||
public async bootstrapSecretStorage({
|
||||
createSecretStorageKey = async () => ({ }),
|
||||
createSecretStorageKey = async () => ({} as IRecoveryKey),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
getKeyBackupPassphrase,
|
||||
}: IBootstrapSecretStorageOpts = {}) {
|
||||
}: ICreateSecretStorageOpts = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
@@ -783,8 +773,7 @@ export class Crypto extends EventEmitter {
|
||||
let newKeyId = null;
|
||||
|
||||
// create a new SSSS key and set it as default
|
||||
const createSSSS = async (opts, privateKey: Uint8Array) => {
|
||||
opts = opts || {};
|
||||
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey: Uint8Array) => {
|
||||
if (privateKey) {
|
||||
opts.key = privateKey;
|
||||
}
|
||||
@@ -800,7 +789,7 @@ export class Crypto extends EventEmitter {
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey(
|
||||
{ keys: { [keyId]: keyInfo } }, "",
|
||||
@@ -880,7 +869,7 @@ export class Crypto extends EventEmitter {
|
||||
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
|
||||
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts: any = {}; // TODO types
|
||||
const opts = {} as IAddSecretStorageKeyOpts;
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
@@ -898,9 +887,7 @@ export class Crypto extends EventEmitter {
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
// store the backup key in secret storage
|
||||
await secretStorage.store(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross-signing key so the key backup can
|
||||
@@ -1274,7 +1261,7 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
private async checkForValidDeviceSignature(
|
||||
userId: string,
|
||||
key: any, // TODO types
|
||||
key: ICrossSigningKey,
|
||||
devices: Record<string, IDevice>,
|
||||
): Promise<string[]> {
|
||||
const deviceIds: string[] = [];
|
||||
@@ -1609,7 +1596,7 @@ export class Crypto extends EventEmitter {
|
||||
*
|
||||
* @param {object} keys The new trusted set of keys
|
||||
*/
|
||||
private async storeTrustedSelfKeys(keys: any): Promise<void> { // TODO types
|
||||
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey>): Promise<void> {
|
||||
if (keys) {
|
||||
this.crossSigningInfo.setKeys(keys);
|
||||
} else {
|
||||
@@ -1864,8 +1851,23 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
if (this.getNeedsNewFallback()) {
|
||||
logger.info("generating fallback key");
|
||||
await this.olmDevice.generateFallbackKey();
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
// if fallbackKeys is non-empty, we've already generated a
|
||||
// fallback key, but it hasn't been published yet, so we
|
||||
// can use that instead of generating a new one
|
||||
if (!fallbackKeys.curve25519 ||
|
||||
Object.keys(fallbackKeys.curve25519).length == 0) {
|
||||
logger.info("generating fallback key");
|
||||
if (this.fallbackCleanup) {
|
||||
// cancel any pending fallback cleanup because generating
|
||||
// a new fallback key will already drop the old fallback
|
||||
// that would have been dropped, and we don't want to kill
|
||||
// the current key
|
||||
clearTimeout(this.fallbackCleanup);
|
||||
delete this.fallbackCleanup;
|
||||
}
|
||||
await this.olmDevice.generateFallbackKey();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("calling uploadOneTimeKeys");
|
||||
@@ -1912,8 +1914,9 @@ export class Crypto extends EventEmitter {
|
||||
private async uploadOneTimeKeys() {
|
||||
const promises = [];
|
||||
|
||||
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||
let fallbackJson: Record<string, IOneTimeKey>;
|
||||
if (this.getNeedsNewFallback()) {
|
||||
fallbackJson = {};
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||
const k = { key, fallback: true };
|
||||
@@ -1938,10 +1941,23 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const res = await this.baseApis.uploadKeysRequest({
|
||||
const requestBody: Record<string, any> = {
|
||||
"one_time_keys": oneTimeJson,
|
||||
"org.matrix.msc2732.fallback_keys": fallbackJson,
|
||||
});
|
||||
};
|
||||
|
||||
if (fallbackJson) {
|
||||
requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
|
||||
requestBody["fallback_keys"] = fallbackJson;
|
||||
}
|
||||
|
||||
const res = await this.baseApis.uploadKeysRequest(requestBody);
|
||||
|
||||
if (fallbackJson) {
|
||||
this.fallbackCleanup = setTimeout(() => {
|
||||
delete this.fallbackCleanup;
|
||||
this.olmDevice.forgetOldFallbackKey();
|
||||
}, 60*60*1000);
|
||||
}
|
||||
|
||||
await this.olmDevice.markKeysAsPublished();
|
||||
return res;
|
||||
@@ -2592,12 +2608,16 @@ export class Crypto extends EventEmitter {
|
||||
* the given users.
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
* @param {boolean} force If true, force a new Olm session to be created. Default false.
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
ensureOlmSessionsForUsers(users: string[]): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
public ensureOlmSessionsForUsers(
|
||||
users: string[],
|
||||
force?: boolean,
|
||||
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser = {};
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
@@ -2622,7 +2642,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2658,7 +2678,7 @@ export class Crypto extends EventEmitter {
|
||||
* @param {Function} opts.progressCallback called with an object which has a stage param
|
||||
* @return {Promise} a promise which resolves once the keys have been imported
|
||||
*/
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: any = {}): Promise<any> { // TODO types
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
const total = keys.length;
|
||||
@@ -2685,7 +2705,7 @@ export class Crypto extends EventEmitter {
|
||||
successes++;
|
||||
if (opts.progressCallback) { updateProgress(); }
|
||||
});
|
||||
}));
|
||||
})).then();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2801,7 +2821,7 @@ export class Crypto extends EventEmitter {
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
redacted_because: decryptedEvent.clearEvent,
|
||||
redacted_because: decryptedEvent.clearEvent as IEvent,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3201,6 +3221,29 @@ export class Crypto extends EventEmitter {
|
||||
createRequest: any, // TODO types
|
||||
isLiveEvent = true,
|
||||
): Promise<void> {
|
||||
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
|
||||
if (event.isSending() && event.status != EventStatus.SENT) {
|
||||
let eventIdListener;
|
||||
let statusListener;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
eventIdListener = resolve;
|
||||
statusListener = () => {
|
||||
if (event.status == EventStatus.CANCELLED) {
|
||||
reject(new Error("Event status set to CANCELLED."));
|
||||
}
|
||||
};
|
||||
event.once("Event.localEventIdReplaced", eventIdListener);
|
||||
event.on("Event.status", statusListener);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("error while waiting for the verification event to be sent: " + err.message);
|
||||
return;
|
||||
} finally {
|
||||
event.removeListener("Event.localEventIdReplaced", eventIdListener);
|
||||
event.removeListener("Event.status", statusListener);
|
||||
}
|
||||
}
|
||||
let request = requestsMap.getRequest(event);
|
||||
let isNewRequest = false;
|
||||
if (!request) {
|
||||
|
||||
@@ -15,6 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from '../randomstring';
|
||||
import { getCrypto } from '../utils';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
@@ -70,11 +74,21 @@ export async function deriveKey(
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits = DEFAULT_BITSIZE,
|
||||
): Promise<Uint8Array> {
|
||||
return subtleCrypto
|
||||
? deriveKeyBrowser(password, salt, iterations, numBits)
|
||||
: deriveKeyNode(password, salt, iterations, numBits);
|
||||
}
|
||||
|
||||
async function deriveKeyBrowser(
|
||||
password: string,
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
const subtleCrypto = global.crypto.subtle;
|
||||
const TextEncoder = global.TextEncoder;
|
||||
if (!subtleCrypto || !TextEncoder) {
|
||||
// TODO: Implement this for node
|
||||
throw new Error("Password-based backup is not avaiable on this platform");
|
||||
}
|
||||
|
||||
@@ -99,3 +113,17 @@ export async function deriveKey(
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
}
|
||||
|
||||
async function deriveKeyNode(
|
||||
password: string,
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
|
||||
}
|
||||
|
||||
@@ -67,5 +67,5 @@ export interface IKeyBackupRestoreResult {
|
||||
|
||||
export interface IKeyBackupRestoreOpts {
|
||||
cacheCompleteCallback?: () => void;
|
||||
progressCallback?: ({ stage: string }) => void;
|
||||
progressCallback?: (progress: { stage: string }) => void;
|
||||
}
|
||||
|
||||
@@ -21,13 +21,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { MatrixClient } from "../client";
|
||||
|
||||
@@ -126,7 +125,7 @@ export async function encryptMessageForDevice(
|
||||
// involved in the session. If we're looking to reduce data transfer in the
|
||||
// future, we could elide them for subsequent messages.
|
||||
|
||||
utils.extend(payload, payloadFields);
|
||||
Object.assign(payload, payloadFields);
|
||||
|
||||
resultsObject[deviceKey] = await olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload),
|
||||
|
||||
@@ -20,8 +20,9 @@ limitations under the License.
|
||||
* @module crypto/verification/Base
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from '../../models/event';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MatrixEvent } from '../../models/event';
|
||||
import { logger } from '../../logger';
|
||||
import { DeviceInfo } from '../deviceinfo';
|
||||
import { newTimeoutError } from "./Error";
|
||||
|
||||
@@ -19,8 +19,10 @@ limitations under the License.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newInvalidMessageError,
|
||||
@@ -29,7 +31,6 @@ import {
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import { logger } from '../../logger';
|
||||
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
|
||||
import { IContent, MatrixEvent } from "../../models/event";
|
||||
|
||||
const START_TYPE = "m.key.verification.start";
|
||||
|
||||
@@ -36,7 +36,7 @@ const M_RELATES_TO = "m.relates_to";
|
||||
* Uses the event id of the initial m.key.verification.request event as a transaction id.
|
||||
*/
|
||||
export class InRoomChannel implements IVerificationChannel {
|
||||
private requestEventId = null;
|
||||
private requestEventId: string = null;
|
||||
|
||||
/**
|
||||
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
|
||||
|
||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import {
|
||||
errorFactory,
|
||||
errorFromEvent,
|
||||
|
||||
@@ -18,8 +18,23 @@ limitations under the License.
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
import {
|
||||
EventType,
|
||||
RelationType,
|
||||
} from "./@types/event";
|
||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_SENDERS = new UnstableValue(
|
||||
"relation_senders",
|
||||
"io.element.relation_senders",
|
||||
);
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_TYPES = new UnstableValue(
|
||||
"relation_types",
|
||||
"io.element.relation_types",
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
@@ -50,6 +65,9 @@ export interface IFilterDefinition {
|
||||
export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
types?: EventType[] | string[];
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
|
||||
}
|
||||
|
||||
interface IStateFilter extends IRoomEventFilter {}
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { parse as parseContentType } from "content-type";
|
||||
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
// we use our own implementation of setTimeout, so that if we get suspended in
|
||||
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
|
||||
// waiting for the delay to elapse.
|
||||
@@ -657,7 +657,7 @@ MatrixHttpApi.prototype = {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = utils.extend({}, opts.headers || {});
|
||||
const headers = Object.assign({}, opts.headers || {});
|
||||
const json = opts.json === undefined ? true : opts.json;
|
||||
let bodyParser = opts.bodyParser;
|
||||
|
||||
|
||||
+3
-1
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import request from "request";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import request from "request";
|
||||
|
||||
matrixcs.request(request);
|
||||
|
||||
@@ -31,3 +32,4 @@ try {
|
||||
|
||||
export * from "./matrix";
|
||||
export default matrixcs;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ limitations under the License.
|
||||
|
||||
/** @module interactive-auth */
|
||||
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
@@ -35,6 +34,7 @@ export interface IInputs {
|
||||
emailAddress?: string;
|
||||
phoneCountry?: string;
|
||||
phoneNumber?: string;
|
||||
registrationToken?: string;
|
||||
}
|
||||
|
||||
export interface IStageStatus {
|
||||
@@ -61,12 +61,13 @@ export enum AuthType {
|
||||
Sso = "m.login.sso",
|
||||
SsoUnstable = "org.matrix.login.sso",
|
||||
Dummy = "m.login.dummy",
|
||||
RegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||
}
|
||||
|
||||
export interface IAuthDict {
|
||||
// [key: string]: any;
|
||||
type?: string;
|
||||
// session?: string; // TODO
|
||||
session?: string;
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
@@ -79,6 +80,7 @@ export interface IAuthDict {
|
||||
// eslint-disable-next-line camelcase
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
registrationToken?: string;
|
||||
}
|
||||
|
||||
class NoAuthFlowFoundError extends Error {
|
||||
@@ -357,12 +359,12 @@ export class InteractiveAuth {
|
||||
}
|
||||
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth;
|
||||
let auth: IAuthDict;
|
||||
if (this.data.session) {
|
||||
auth = {
|
||||
session: this.data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
Object.assign(auth, authData);
|
||||
} else {
|
||||
auth = authData;
|
||||
}
|
||||
@@ -449,6 +451,7 @@ export class InteractiveAuth {
|
||||
} catch (e) {
|
||||
this.attemptAuthDeferred.reject(e);
|
||||
this.attemptAuthDeferred = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ function extendLogger(logger: PrefixedLogger) {
|
||||
|
||||
extendLogger(logger);
|
||||
|
||||
function getPrefixedLogger(prefix): PrefixedLogger {
|
||||
function getPrefixedLogger(prefix: string): 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.
|
||||
|
||||
+6
-1
@@ -46,6 +46,11 @@ export * from "./store/session/webstorage";
|
||||
export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export * from "./content-repo";
|
||||
export * from './@types/event';
|
||||
export * from './@types/PushRules';
|
||||
export * from './@types/requests';
|
||||
export * from './@types/search';
|
||||
export * from './models/room-summary';
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
@@ -116,7 +121,7 @@ export interface ICryptoCallbacks {
|
||||
) => Promise<string>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (Uint8Array) => void,
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MatrixClient } from "../client";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import type { ReadStream } from "fs";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
@@ -37,7 +38,11 @@ export class MSC3089Branch {
|
||||
* The file ID.
|
||||
*/
|
||||
public get id(): string {
|
||||
return this.indexEvent.getStateKey();
|
||||
const stateKey = this.indexEvent.getStateKey();
|
||||
if (!stateKey) {
|
||||
throw new Error("State key not found for branch");
|
||||
}
|
||||
return stateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +125,10 @@ export class MSC3089Branch {
|
||||
const file = event.getOriginalContent()['file'];
|
||||
const httpUrl = this.client.mxcUrlToHttp(file['url']);
|
||||
|
||||
if (!httpUrl) {
|
||||
throw new Error(`No HTTP URL available for ${file['url']}`);
|
||||
}
|
||||
|
||||
return { info: file, httpUrl: httpUrl };
|
||||
}
|
||||
|
||||
@@ -142,16 +151,16 @@ export class MSC3089Branch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new version of this file.
|
||||
* Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent().
|
||||
* @param {string} name The name of the file.
|
||||
* @param {ArrayBuffer} encryptedContents The encrypted contents.
|
||||
* @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents.
|
||||
* @param {Partial<IEncryptedFile>} info The encrypted file information.
|
||||
* @param {IContent} additionalContent Optional event content fields to include in the message.
|
||||
* @returns {Promise<void>} Resolves when uploaded.
|
||||
*/
|
||||
public async createNewVersion(
|
||||
name: string,
|
||||
encryptedContents: ArrayBuffer,
|
||||
encryptedContents: File | String | Buffer | ReadStream | Blob,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<void> {
|
||||
@@ -198,7 +207,7 @@ export class MSC3089Branch {
|
||||
// XXX: This is a very inefficient search, but it's the best we can do with the
|
||||
// relations structure we have in the SDK. As of writing, it is not worth the
|
||||
// investment in improving the structure.
|
||||
let childEvent: MatrixEvent;
|
||||
let childEvent: MatrixEvent | undefined;
|
||||
let parentEvent = await this.getFileEvent();
|
||||
do {
|
||||
childEvent = timelineEvents.find(e => e.replacingEventId() === parentEvent.getId());
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import promiseRetry from "p-retry";
|
||||
|
||||
import { MatrixClient } from "../client";
|
||||
import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
|
||||
import { Room } from "./room";
|
||||
@@ -28,9 +30,9 @@ import {
|
||||
simpleRetryOperation,
|
||||
} from "../utils";
|
||||
import { MSC3089Branch } from "./MSC3089Branch";
|
||||
import promiseRetry from "p-retry";
|
||||
import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
|
||||
import { ISendEventResponse } from "../@types/requests";
|
||||
import type { ReadStream } from "fs";
|
||||
|
||||
/**
|
||||
* The recommended defaults for a tree space's power levels. Note that this
|
||||
@@ -244,8 +246,11 @@ export class MSC3089TreeSpace {
|
||||
const children = this.room.currentState.getStateEvents(EventType.SpaceChild);
|
||||
for (const child of children) {
|
||||
try {
|
||||
const tree = this.client.unstableGetFileTreeSpace(child.getStateKey());
|
||||
if (tree) trees.push(tree);
|
||||
const stateKey = child.getStateKey();
|
||||
if (stateKey) {
|
||||
const tree = this.client.unstableGetFileTreeSpace(stateKey);
|
||||
if (tree) trees.push(tree);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
|
||||
}
|
||||
@@ -257,9 +262,9 @@ export class MSC3089TreeSpace {
|
||||
* Gets a subdirectory of a given ID under this tree space. Note that this will not recurse
|
||||
* into children and instead only look one level deep.
|
||||
* @param {string} roomId The room ID (directory ID) to find.
|
||||
* @returns {MSC3089TreeSpace} The directory, or falsy if not found.
|
||||
* @returns {MSC3089TreeSpace | undefined} The directory, or undefined if not found.
|
||||
*/
|
||||
public getDirectory(roomId: string): MSC3089TreeSpace {
|
||||
public getDirectory(roomId: string): MSC3089TreeSpace | undefined {
|
||||
return this.getDirectories().find(r => r.roomId === roomId);
|
||||
}
|
||||
|
||||
@@ -277,8 +282,12 @@ export class MSC3089TreeSpace {
|
||||
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
|
||||
for (const member of members) {
|
||||
const isNotUs = member.getStateKey() !== this.client.getUserId();
|
||||
if (isNotUs && kickMemberships.includes(member.getContent()['membership'])) {
|
||||
await this.client.kick(this.roomId, member.getStateKey(), "Room deleted");
|
||||
if (isNotUs && kickMemberships.includes(member.getContent<string>()['membership'])) {
|
||||
const stateKey = member.getStateKey();
|
||||
if (!stateKey) {
|
||||
throw new Error("State key not found for branch");
|
||||
}
|
||||
await this.client.kick(this.roomId, stateKey, "Room deleted");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +296,8 @@ export class MSC3089TreeSpace {
|
||||
|
||||
private getOrderedChildren(children: MatrixEvent[]): { roomId: string, order: string }[] {
|
||||
const ordered: { roomId: string, order: string }[] = children
|
||||
.map(c => ({ roomId: c.getStateKey(), order: c.getContent()['order'] }));
|
||||
.map(c => ({ roomId: c.getStateKey(), order: c.getContent()['order'] }))
|
||||
.filter(c => c.roomId) as { roomId: string, order: string }[];
|
||||
ordered.sort((a, b) => {
|
||||
if (a.order && !b.order) {
|
||||
return -1;
|
||||
@@ -320,7 +330,9 @@ export class MSC3089TreeSpace {
|
||||
|
||||
// XXX: We are assuming the parent is a valid tree space.
|
||||
// We probably don't need to validate the parent room state for this usecase though.
|
||||
const parentRoom = this.client.getRoom(parent.getStateKey());
|
||||
const stateKey = parent.getStateKey();
|
||||
if (!stateKey) throw new Error("No state key found for parent");
|
||||
const parentRoom = this.client.getRoom(stateKey);
|
||||
if (!parentRoom) throw new Error("Unable to locate room for parent");
|
||||
|
||||
return parentRoom;
|
||||
@@ -412,7 +424,7 @@ export class MSC3089TreeSpace {
|
||||
// We were asked by the order algorithm to prepare the moving space for a landing
|
||||
// in the undefined order part of the order array, which means we need to update the
|
||||
// spaces that come before it with a stable order value.
|
||||
let lastOrder: string;
|
||||
let lastOrder: string | undefined;
|
||||
for (let i = 0; i <= index; i++) {
|
||||
const target = ordered[i];
|
||||
if (i === 0) {
|
||||
@@ -431,7 +443,9 @@ export class MSC3089TreeSpace {
|
||||
lastOrder = target.order;
|
||||
}
|
||||
}
|
||||
newOrder = nextString(lastOrder);
|
||||
if (lastOrder) {
|
||||
newOrder = nextString(lastOrder);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Deal with order conflicts by reordering
|
||||
@@ -449,21 +463,23 @@ export class MSC3089TreeSpace {
|
||||
|
||||
/**
|
||||
* Creates (uploads) a new file to this tree. The file must have already been encrypted for the room.
|
||||
* The file contents are in a type that is compatible with MatrixClient.uploadContent().
|
||||
* @param {string} name The name of the file.
|
||||
* @param {ArrayBuffer} encryptedContents The encrypted contents.
|
||||
* @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents.
|
||||
* @param {Partial<IEncryptedFile>} info The encrypted file information.
|
||||
* @param {IContent} additionalContent Optional event content fields to include in the message.
|
||||
* @returns {Promise<ISendEventResponse>} Resolves to the file event's sent response.
|
||||
*/
|
||||
public async createFile(
|
||||
name: string,
|
||||
encryptedContents: ArrayBuffer,
|
||||
encryptedContents: File | String | Buffer | ReadStream | Blob,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<ISendEventResponse> {
|
||||
const mxc = await this.client.uploadContent(new Blob([encryptedContents]), {
|
||||
const mxc = await this.client.uploadContent(encryptedContents, {
|
||||
includeFilename: false,
|
||||
onlyContentUri: true,
|
||||
rawResponse: false, // make this explicit otherwise behaviour is different on browser vs NodeJS
|
||||
});
|
||||
info.url = mxc;
|
||||
|
||||
@@ -499,9 +515,9 @@ export class MSC3089TreeSpace {
|
||||
/**
|
||||
* Retrieves a file from the tree.
|
||||
* @param {string} fileEventId The event ID of the file.
|
||||
* @returns {MSC3089Branch} The file, or falsy if not found.
|
||||
* @returns {MSC3089Branch | null} The file, or null if not found.
|
||||
*/
|
||||
public getFile(fileEventId: string): MSC3089Branch {
|
||||
public getFile(fileEventId: string): MSC3089Branch | null {
|
||||
const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId);
|
||||
return branch ? new MSC3089Branch(this.client, branch, this) : null;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,12 @@ import { Room } from "./room";
|
||||
import { Filter } from "../filter";
|
||||
import { EventType, RelationType } from "../@types/event";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread } from "./thread";
|
||||
|
||||
// var DEBUG = false;
|
||||
const DEBUG = true;
|
||||
|
||||
let debuglog;
|
||||
let debuglog: (...args: any[]) => void;
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
debuglog = logger.log.bind(logger);
|
||||
@@ -44,6 +45,7 @@ interface IOpts {
|
||||
timelineSupport?: boolean;
|
||||
filter?: Filter;
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
pendingEvents?: boolean;
|
||||
}
|
||||
|
||||
export enum DuplicateStrategy {
|
||||
@@ -54,6 +56,7 @@ export enum DuplicateStrategy {
|
||||
export class EventTimelineSet extends EventEmitter {
|
||||
private readonly timelineSupport: boolean;
|
||||
private unstableClientRelationAggregation: boolean;
|
||||
private displayPendingEvents: boolean;
|
||||
private liveTimeline: EventTimeline;
|
||||
private timelines: EventTimeline[];
|
||||
private _eventIdToTimeline: Record<string, EventTimeline>;
|
||||
@@ -102,6 +105,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.liveTimeline = new EventTimeline(this);
|
||||
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
this.displayPendingEvents = opts.pendingEvents !== false;
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this.timelines = [this.liveTimeline];
|
||||
@@ -150,18 +154,18 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (!this.room) {
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
if (!this.room || !this.displayPendingEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pendingEvents = this.room.getPendingEvents(thread);
|
||||
if (this.filter) {
|
||||
return this.filter.filterRoomTimeline(this.room.getPendingEvents());
|
||||
return this.filter.filterRoomTimeline(pendingEvents);
|
||||
} else {
|
||||
return this.room.getPendingEvents();
|
||||
return pendingEvents;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
*
|
||||
|
||||
+44
-49
@@ -28,14 +28,14 @@ import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
|
||||
} from "../@types/event";
|
||||
import { Crypto } from "../crypto";
|
||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { IActionsObject } from '../pushprocessor';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { MatrixError } from "../http-api";
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
@@ -119,31 +119,19 @@ interface IAggregatedRelation {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface IEventRelation {
|
||||
export interface IEventRelation {
|
||||
rel_type: RelationType | string;
|
||||
event_id: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface IDecryptionResult {
|
||||
clearEvent: {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
content: IContent;
|
||||
unsigned?: IUnsigned;
|
||||
};
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export interface IClearEvent {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
|
||||
unsigned?: IUnsigned;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface IKeyRequestRecipient {
|
||||
userId: string;
|
||||
@@ -216,14 +204,14 @@ export class MatrixEvent extends EventEmitter {
|
||||
public sender: RoomMember = null;
|
||||
public target: RoomMember = null;
|
||||
public status: EventStatus = null;
|
||||
public error = null;
|
||||
public error: MatrixError = null;
|
||||
public forwardLooking = true;
|
||||
|
||||
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||
* `Crypto` will set this the `VerificationRequest` for the event
|
||||
* so it can be easily accessed from the timeline.
|
||||
*/
|
||||
public verificationRequest = null;
|
||||
public verificationRequest: VerificationRequest = null;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
|
||||
@@ -419,38 +407,45 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the event ID of the replied event
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string {
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === RelationType.Thread) {
|
||||
return relatesTo.event_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRelation(): boolean {
|
||||
return !!this.threadRootId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
// TODO, change the inner working of this getter for it to use the
|
||||
// bundled relationship return on the event, view MSC3440
|
||||
const thread = this.getThread();
|
||||
return thread?.id === this.getId();
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId || this.relationEventId;
|
||||
}
|
||||
|
||||
public get replyEventId(): string {
|
||||
const relations = this.getWireContent()["m.relates_to"];
|
||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Determines whether a reply should be rendered in a thread
|
||||
* or in the main room timeline
|
||||
*/
|
||||
public get replyInThread(): boolean {
|
||||
/**
|
||||
* UNSTABLE_ELEMENT_REPLY_IN_THREAD can live either
|
||||
* at the m.relates_to and m.in_reply_to level
|
||||
* This will likely change once we settle on a
|
||||
* way to achieve threads
|
||||
* TODO: Clean this up once we have a clear way forward
|
||||
*/
|
||||
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
const replyTo = relatesTo?.["m.in_reply_to"];
|
||||
|
||||
return relatesTo?.[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]
|
||||
|| (this.replyEventId && replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|
||||
|| this.thread instanceof Thread;
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId
|
||||
|| this.getWireContent()["m.relates_to"]?.event_id;
|
||||
public get relationEventId(): string {
|
||||
return this.getWireContent()
|
||||
?.["m.relates_to"]
|
||||
?.event_id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -684,8 +679,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
while (true) {
|
||||
this.retryDecryption = false;
|
||||
|
||||
let res;
|
||||
let err;
|
||||
let res: IEventDecryptionResult;
|
||||
let err: Error;
|
||||
try {
|
||||
if (!crypto) {
|
||||
res = this.badEncryptedMessage("Encryption not enabled");
|
||||
@@ -773,7 +768,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private badEncryptedMessage(reason: string): IDecryptionResult {
|
||||
private badEncryptedMessage(reason: string): IEventDecryptionResult {
|
||||
return {
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
@@ -797,7 +792,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @param {module:crypto~EventDecryptionResult} decryptionResult
|
||||
* the decryption result, including the plaintext and some key info
|
||||
*/
|
||||
private setClearData(decryptionResult: IDecryptionResult): void {
|
||||
private setClearData(decryptionResult: IEventDecryptionResult): void {
|
||||
this.clearEvent = decryptionResult.clearEvent;
|
||||
this.senderCurve25519Key =
|
||||
decryptionResult.senderCurve25519Key || null;
|
||||
|
||||
+2
-1
@@ -20,9 +20,10 @@ limitations under the License.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
|
||||
import * as utils from "../utils";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import * as utils from "../utils";
|
||||
|
||||
/**
|
||||
* Construct a new Group.
|
||||
*
|
||||
|
||||
@@ -131,7 +131,9 @@ export class RoomMember extends EventEmitter {
|
||||
this.disambiguate,
|
||||
);
|
||||
|
||||
this.rawDisplayName = event.getDirectionalContent().displayname;
|
||||
// not quite raw: we strip direction override chars so it can safely be inserted into
|
||||
// blocks of text without breaking the text direction
|
||||
this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname);
|
||||
if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) {
|
||||
this.rawDisplayName = this.userId;
|
||||
}
|
||||
@@ -355,7 +357,7 @@ function calculateDisplayName(
|
||||
roomState: RoomState,
|
||||
disambiguate: boolean,
|
||||
): string {
|
||||
if (disambiguate) return displayName + " (" + selfUserId + ")";
|
||||
if (disambiguate) return utils.removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")";
|
||||
|
||||
if (!displayName || displayName === selfUserId) return selfUserId;
|
||||
|
||||
@@ -363,7 +365,18 @@ function calculateDisplayName(
|
||||
// after stripping it of zero width characters and padding spaces
|
||||
if (!utils.removeHiddenChars(displayName)) return selfUserId;
|
||||
|
||||
return displayName;
|
||||
// We always strip the direction override characters (LRO and RLO).
|
||||
// These override the text direction for all subsequent characters
|
||||
// in the paragraph so if display names contained these, they'd
|
||||
// need to be wrapped in something to prevent this from leaking out
|
||||
// (which we can do in HTML but not text) or we'd need to add
|
||||
// control characters to the string to reset any overrides (eg.
|
||||
// adding PDF characters at the end). As far as we can see,
|
||||
// there should be no reason these would be necessary - rtl display
|
||||
// names should flip into the correct direction automatically based on
|
||||
// the characters, and you can still embed rtl in ltr or vice versa
|
||||
// with the embed chars or marker chars.
|
||||
return utils.removeDirectionOverrideChars(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+170
-149
@@ -48,19 +48,19 @@ const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
||||
|
||||
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: string): MatrixEvent {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
// This is really ugly because JS has no way to express an object literal
|
||||
// where the name of a key comes from an expression
|
||||
const fakeReceipt = {
|
||||
content: {},
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[event.getId()]: {
|
||||
[receiptType]: {
|
||||
[userId]: {
|
||||
ts: event.getTs(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
room_id: event.getRoomId(),
|
||||
};
|
||||
fakeReceipt.content[event.getId()] = {};
|
||||
fakeReceipt.content[event.getId()][receiptType] = {};
|
||||
fakeReceipt.content[event.getId()][receiptType][userId] = {
|
||||
ts: event.getTs(),
|
||||
};
|
||||
return new MatrixEvent(fakeReceipt);
|
||||
});
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
@@ -149,7 +149,7 @@ export class Room extends EventEmitter {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Set<Thread>();
|
||||
public threads = new Map<string, Thread>();
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
@@ -225,12 +225,6 @@ export class Room extends EventEmitter {
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
throw new Error(
|
||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
||||
"'detached'. Got: '" + opts.pendingEventOrdering + "'",
|
||||
);
|
||||
}
|
||||
|
||||
this.name = roomId;
|
||||
|
||||
@@ -241,12 +235,12 @@ export class Room extends EventEmitter {
|
||||
|
||||
this.fixUpLegacyTimelineFields();
|
||||
|
||||
if (this.opts.pendingEventOrdering == "detached") {
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
|
||||
this.pendingEventList = [];
|
||||
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
|
||||
if (serializedPendingEventList) {
|
||||
JSON.parse(serializedPendingEventList)
|
||||
.forEach(async serializedEvent => {
|
||||
.forEach(async (serializedEvent: Partial<IEvent>) => {
|
||||
const event = new MatrixEvent(serializedEvent);
|
||||
if (event.getType() === EventType.RoomMessageEncrypted) {
|
||||
await event.attemptDecryption(this.client.crypto);
|
||||
@@ -452,14 +446,16 @@ export class Room extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (this.opts.pendingEventOrdering !== "detached") {
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
throw new Error(
|
||||
"Cannot call getPendingEvents with pendingEventOrdering == " +
|
||||
this.opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
return this.pendingEventList;
|
||||
return this.pendingEventList.filter(event => {
|
||||
return !thread || thread.id === event.threadRootId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,7 +465,7 @@ export class Room extends EventEmitter {
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
public removePendingEvent(eventId: string): boolean {
|
||||
if (this.opts.pendingEventOrdering !== "detached") {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
throw new Error(
|
||||
"Cannot call removePendingEvent with pendingEventOrdering == " +
|
||||
this.opts.pendingEventOrdering);
|
||||
@@ -495,7 +491,7 @@ export class Room extends EventEmitter {
|
||||
* @return {boolean}
|
||||
*/
|
||||
public hasPendingEvent(eventId: string): boolean {
|
||||
if (this.opts.pendingEventOrdering !== "detached") {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -509,7 +505,7 @@ export class Room extends EventEmitter {
|
||||
* @return {MatrixEvent}
|
||||
*/
|
||||
public getPendingEvent(eventId: string): MatrixEvent | null {
|
||||
if (this.opts.pendingEventOrdering !== "detached") {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -672,7 +668,13 @@ export class Room extends EventEmitter {
|
||||
// were the members loaded from the server?
|
||||
let fromServer = false;
|
||||
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
if (rawMembersEvents === null) {
|
||||
// If the room is encrypted, we always fetch members from the server at
|
||||
// least once, in case the latest state wasn't persisted properly. Note
|
||||
// that this function is only called once (unless loading the members
|
||||
// fails), since loadMembersIfNeeded always returns this.membersPromise
|
||||
// if set, which will be the result of the first (successful) call.
|
||||
if (rawMembersEvents === null ||
|
||||
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
logger.log(`LL: got ${rawMembersEvents.length} ` +
|
||||
@@ -850,7 +852,13 @@ export class Room extends EventEmitter {
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
public getTimelineForEvent(eventId: string): EventTimeline {
|
||||
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
|
||||
const event = this.findEventById(eventId);
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
return thread.timelineSet.getLiveTimeline();
|
||||
} else {
|
||||
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -989,14 +997,14 @@ export class Room extends EventEmitter {
|
||||
* @return {array} The room's alias as an array of strings
|
||||
*/
|
||||
public getAliases(): string[] {
|
||||
const aliasStrings = [];
|
||||
const aliasStrings: string[] = [];
|
||||
|
||||
const aliasEvents = this.currentState.getStateEvents(EventType.RoomAliases);
|
||||
if (aliasEvents) {
|
||||
for (let i = 0; i < aliasEvents.length; ++i) {
|
||||
const aliasEvent = aliasEvents[i];
|
||||
if (Array.isArray(aliasEvent.getContent().aliases)) {
|
||||
const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
|
||||
const filteredAliases = aliasEvent.getContent<{ aliases: string[] }>().aliases.filter(a => {
|
||||
if (typeof(a) !== "string") return false;
|
||||
if (a[0] !== '#') return false;
|
||||
if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false;
|
||||
@@ -1068,19 +1076,6 @@ export class Room extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public addThread(thread: Thread): Set<Thread> {
|
||||
this.threads.add(thread);
|
||||
if (!thread.ready) {
|
||||
thread.once(ThreadEvent.Ready, this.dedupeThreads);
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
|
||||
}
|
||||
return this.threads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@@ -1097,26 +1092,6 @@ export class Room extends EventEmitter {
|
||||
return Array.from(this.threads.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Two threads starting from a different child event can end up
|
||||
* with the same event root. This method ensures that the duplicates
|
||||
* are removed
|
||||
* @experimental
|
||||
*/
|
||||
private dedupeThreads = (readyThread): void => {
|
||||
const deduped = Array.from(this.threads).reduce((dedupedThreads, thread) => {
|
||||
if (dedupedThreads.has(thread.id)) {
|
||||
dedupedThreads.get(thread.id).merge(thread);
|
||||
} else {
|
||||
dedupedThreads.set(thread.id, thread);
|
||||
}
|
||||
|
||||
return dedupedThreads;
|
||||
}, new Map<string, Thread>());
|
||||
|
||||
this.threads = new Set<Thread>(deduped.values());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a member from the current room state.
|
||||
* @param {string} userId The user ID of the member.
|
||||
@@ -1293,21 +1268,55 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public findThreadForEvent(event: MatrixEvent): Thread {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.isThreadRelation) {
|
||||
return this.threads.get(event.threadRootId);
|
||||
} else if (event.isThreadRoot) {
|
||||
return this.threads.get(event.getId());
|
||||
} else {
|
||||
const parentEvent = this.findEventById(event.parentEventId);
|
||||
return this.findThreadForEvent(parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public addThreadedEvent(event: MatrixEvent): void {
|
||||
let thread = this.findEventById(event.parentEventId)?.getThread();
|
||||
public async addThreadedEvent(event: MatrixEvent): Promise<void> {
|
||||
let thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.addEvent(event);
|
||||
} else {
|
||||
thread = new Thread([event], this, this.client);
|
||||
const events = [event];
|
||||
let rootEvent = this.findEventById(event.threadRootId);
|
||||
// If the rootEvent does not exist in the current sync, then look for
|
||||
// it over the network
|
||||
if (!rootEvent) {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||
rootEvent = new MatrixEvent(eventData);
|
||||
}
|
||||
events.unshift(rootEvent);
|
||||
thread = new Thread(events, this, this.client);
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
|
||||
this.emit(ThreadEvent.New, thread);
|
||||
}
|
||||
|
||||
if (!this.threads.has(thread)) {
|
||||
this.addThread(thread);
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1406,13 +1415,6 @@ export class Room extends EventEmitter {
|
||||
* unique transaction id.
|
||||
*/
|
||||
public addPendingEvent(event: MatrixEvent, txnId: string): void {
|
||||
// TODO: Enable "pending events" for threads
|
||||
// There's a fair few things to update to make them work with Threads
|
||||
// Will get back to it when the plan is to build a more polished UI ready for production
|
||||
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
|
||||
throw new Error("addPendingEvent called on an event with status " +
|
||||
event.status);
|
||||
@@ -1429,8 +1431,8 @@ export class Room extends EventEmitter {
|
||||
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
|
||||
|
||||
this.txnToEvent[txnId] = event;
|
||||
|
||||
if (this.opts.pendingEventOrdering == "detached") {
|
||||
const thread = this.threads.get(event.threadRootId);
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
|
||||
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
@@ -1449,7 +1451,7 @@ export class Room extends EventEmitter {
|
||||
let redactedEvent = this.pendingEventList &&
|
||||
this.pendingEventList.find(e => e.getId() === redactId);
|
||||
if (!redactedEvent) {
|
||||
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
|
||||
redactedEvent = this.findEventById(redactId);
|
||||
}
|
||||
if (redactedEvent) {
|
||||
redactedEvent.markLocallyRedacted(event);
|
||||
@@ -1457,16 +1459,21 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
if (thread) {
|
||||
thread.timelineSet.addEventToTimeline(event,
|
||||
thread.timelineSet.getLiveTimeline(), false);
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1524,16 +1531,21 @@ export class Room extends EventEmitter {
|
||||
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
|
||||
*/
|
||||
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
||||
// TODO: We should consider whether this means it would be a better
|
||||
// design to lift the relations handling up to the room instead.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.aggregateRelations(event);
|
||||
} else {
|
||||
// TODO: We should consider whether this means it would be a better
|
||||
// design to lift the relations handling up to the room instead.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
} else {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
} else {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1574,25 +1586,22 @@ export class Room extends EventEmitter {
|
||||
// any, which is good, because we don't want to try decoding it again).
|
||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
const thread = this.threads.get(remoteEvent.threadRootId);
|
||||
if (thread) {
|
||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
|
||||
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
||||
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
||||
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", localEvent, this,
|
||||
oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
public findThreadByEventId(eventId: string): Thread {
|
||||
for (const thread of this.threads) {
|
||||
if (thread.has(eventId)) {
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status / event id on a pending event, to reflect its transmission
|
||||
* progress.
|
||||
@@ -1619,7 +1628,7 @@ export class Room extends EventEmitter {
|
||||
|
||||
// SENT races against /sync, so we have to special-case it.
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
|
||||
const timeline = this.getTimelineForEvent(newEventId);
|
||||
if (timeline) {
|
||||
// we've already received the event via the event stream.
|
||||
// nothing more to do here.
|
||||
@@ -1647,11 +1656,16 @@ export class Room extends EventEmitter {
|
||||
// update the event id
|
||||
event.replaceLocalEventId(newEventId);
|
||||
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
this.timelineSets[i].replaceEventId(oldEventId, newEventId);
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||
} else {
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
this.timelineSets[i].replaceEventId(oldEventId, newEventId);
|
||||
}
|
||||
}
|
||||
} else if (newStatus == EventStatus.CANCELLED) {
|
||||
// remove it from the pending event list, or the timeline.
|
||||
@@ -1733,6 +1747,10 @@ export class Room extends EventEmitter {
|
||||
// TODO: We should have a filter to say "only add state event
|
||||
// types X Y Z to the timeline".
|
||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||
const thread = this.threads.get(events[i].getId());
|
||||
if (thread) {
|
||||
thread.addEvent(events[i], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1834,16 +1852,7 @@ export class Room extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the event that a given user has read up to, or null if we
|
||||
* have received no read receipts from them.
|
||||
* @param {String} userId The user ID to get read receipt event ID for
|
||||
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
|
||||
* sent by the server, not implicit ones generated
|
||||
* by the JS SDK.
|
||||
* @return {String} ID of the latest event that the given user has read, or null.
|
||||
*/
|
||||
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
||||
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
|
||||
let receipts = this.receipts;
|
||||
if (ignoreSynthesized) {
|
||||
receipts = this.realReceipts;
|
||||
@@ -1856,7 +1865,21 @@ export class Room extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
return receipts["m.read"][userId].eventId;
|
||||
return receipts["m.read"][userId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the event that a given user has read up to, or null if we
|
||||
* have received no read receipts from them.
|
||||
* @param {String} userId The user ID to get read receipt event ID for
|
||||
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
|
||||
* sent by the server, not implicit ones generated
|
||||
* by the JS SDK.
|
||||
* @return {String} ID of the latest event that the given user has read, or null.
|
||||
*/
|
||||
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
||||
const readReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized);
|
||||
return readReceipt?.eventId ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1969,7 +1992,7 @@ export class Room extends EventEmitter {
|
||||
* @return {Object} Map of receipts by event ID
|
||||
*/
|
||||
private buildReceiptCache(receipts: Receipts): ReceiptCache {
|
||||
const receiptCacheByEventId = {};
|
||||
const receiptCacheByEventId: ReceiptCache = {};
|
||||
Object.keys(receipts).forEach(function(receiptType) {
|
||||
Object.keys(receipts[receiptType]).forEach(function(userId) {
|
||||
const receipt = receipts[receiptType][userId];
|
||||
@@ -2162,7 +2185,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
// get members that are NOT ourselves and are actually in the room.
|
||||
let otherNames = null;
|
||||
let otherNames: string[] = null;
|
||||
if (this.summaryHeroes) {
|
||||
// if we have a summary, the member state events
|
||||
// should be in the room state
|
||||
@@ -2243,31 +2266,29 @@ function pendingEventsKey(roomId: string): string {
|
||||
|
||||
/* a map from current event status to a list of allowed next statuses
|
||||
*/
|
||||
const ALLOWED_TRANSITIONS = {};
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.NOT_SENT,
|
||||
];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.SENDING] = [
|
||||
EventStatus.ENCRYPTING,
|
||||
EventStatus.QUEUED,
|
||||
EventStatus.NOT_SENT,
|
||||
EventStatus.SENT,
|
||||
];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
|
||||
[EventStatus.SENDING, EventStatus.CANCELLED];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.SENT] =
|
||||
[];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] =
|
||||
[EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
|
||||
[];
|
||||
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
|
||||
[EventStatus.ENCRYPTING]: [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.NOT_SENT,
|
||||
],
|
||||
[EventStatus.SENDING]: [
|
||||
EventStatus.ENCRYPTING,
|
||||
EventStatus.QUEUED,
|
||||
EventStatus.NOT_SENT,
|
||||
EventStatus.SENT,
|
||||
],
|
||||
[EventStatus.QUEUED]: [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.CANCELLED,
|
||||
],
|
||||
[EventStatus.SENT]: [],
|
||||
[EventStatus.NOT_SENT]: [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.QUEUED,
|
||||
EventStatus.CANCELLED,
|
||||
],
|
||||
[EventStatus.CANCELLED]: [],
|
||||
};
|
||||
|
||||
// TODO i18n
|
||||
function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
|
||||
|
||||
+48
-104
@@ -14,27 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { MatrixClient } from "../matrix";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
Ready = "Thread.ready",
|
||||
Update = "Thread.update"
|
||||
}
|
||||
|
||||
interface ISerialisedThread {
|
||||
id: string;
|
||||
tails: string[];
|
||||
Update = "Thread.update",
|
||||
NewReply = "Thread.newReply",
|
||||
ViewThread = "Thred.viewThread",
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends EventEmitter {
|
||||
export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* A reference to the event ID at the top of the thread
|
||||
*/
|
||||
@@ -42,8 +40,9 @@ export class Thread extends EventEmitter {
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
public readonly tail = new Set<string>();
|
||||
public readonly timelineSet: EventTimelineSet;
|
||||
public readonly timelineSet;
|
||||
|
||||
private _currentUserParticipated = false;
|
||||
|
||||
constructor(
|
||||
events: MatrixEvent[] = [],
|
||||
@@ -51,13 +50,27 @@ export class Thread extends EventEmitter {
|
||||
public readonly client: MatrixClient,
|
||||
) {
|
||||
super();
|
||||
this.timelineSet = new EventTimelineSet(room, {
|
||||
if (events.length === 0) {
|
||||
throw new Error("Can't create an empty thread");
|
||||
}
|
||||
|
||||
this.timelineSet = new EventTimelineSet(this.room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
pendingEvents: true,
|
||||
});
|
||||
events.forEach(event => this.addEvent(event));
|
||||
|
||||
room.on("Room.localEchoUpdated", this.onEcho);
|
||||
room.on("Room.timeline", this.onEcho);
|
||||
}
|
||||
|
||||
onEcho = (event: MatrixEvent) => {
|
||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
@@ -69,13 +82,12 @@ export class Thread extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tail.has(event.replyEventId)) {
|
||||
this.tail.delete(event.replyEventId);
|
||||
}
|
||||
this.tail.add(event.getId());
|
||||
|
||||
if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
|
||||
this.root = event.getId();
|
||||
if (!this.root) {
|
||||
if (event.isThreadRelation) {
|
||||
this.root = event.threadRootId;
|
||||
} else {
|
||||
this.root = event.getId();
|
||||
}
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
@@ -93,57 +105,18 @@ export class Thread extends EventEmitter {
|
||||
roomState,
|
||||
);
|
||||
|
||||
if (this.ready) {
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the reply chain with all events
|
||||
* missing from the current sync data
|
||||
* Will fire "Thread.ready"
|
||||
*/
|
||||
public async fetchReplyChain(): Promise<void> {
|
||||
if (!this.ready) {
|
||||
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
|
||||
if (!mxEvent) {
|
||||
mxEvent = await this.fetchEventById(
|
||||
this.rootEvent.getRoomId(),
|
||||
this.rootEvent.replyEventId,
|
||||
);
|
||||
}
|
||||
|
||||
this.addEvent(mxEvent, true);
|
||||
if (mxEvent.replyEventId) {
|
||||
await this.fetchReplyChain();
|
||||
} else {
|
||||
await this.decryptEvents();
|
||||
this.emit(ThreadEvent.Ready, this);
|
||||
}
|
||||
if (event.isThreadRelation) {
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptEvents(): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
|
||||
return this.client.decryptEventIfNeeded(event, {});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an event over the network
|
||||
*/
|
||||
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
|
||||
const response = await this.client.http.authedRequest(
|
||||
undefined,
|
||||
"GET",
|
||||
`/rooms/${roomId}/event/${eventId}`,
|
||||
);
|
||||
return new MatrixEvent(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an event by ID in the current thread
|
||||
*/
|
||||
@@ -152,10 +125,12 @@ export class Thread extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines thread's ready status
|
||||
* Return last reply to the thread
|
||||
*/
|
||||
public get ready(): boolean {
|
||||
return this.rootEvent.replyEventId === undefined;
|
||||
public get lastReply(): MatrixEvent {
|
||||
const threadReplies = this.events
|
||||
.filter(event => event.isThreadRelation);
|
||||
return threadReplies[threadReplies.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,20 +153,13 @@ export class Thread extends EventEmitter {
|
||||
|
||||
/**
|
||||
* The number of messages in the thread
|
||||
* Only count rel_type=m.thread as we want to
|
||||
* exclude annotations from that number
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.events.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of mxid participating to the thread
|
||||
*/
|
||||
public get participants(): Set<string> {
|
||||
const participants = new Set<string>();
|
||||
this.events.forEach(event => {
|
||||
participants.add(event.getSender());
|
||||
});
|
||||
return participants;
|
||||
return this.events
|
||||
.filter(event => event.isThreadRelation)
|
||||
.length;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,31 +185,7 @@ export class Thread extends EventEmitter {
|
||||
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
|
||||
}
|
||||
|
||||
public toJson(): ISerialisedThread {
|
||||
return {
|
||||
id: this.id,
|
||||
tails: Array.from(this.tail),
|
||||
};
|
||||
}
|
||||
|
||||
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.once(event, listener);
|
||||
return this;
|
||||
}
|
||||
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.off(event, listener);
|
||||
return this;
|
||||
}
|
||||
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.addListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.removeListener(event, listener);
|
||||
return this;
|
||||
public get hasCurrentUserParticipated(): boolean {
|
||||
return this._currentUserParticipated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
enum EventEmitterEvents {
|
||||
NewListener = "newListener",
|
||||
RemoveListener = "removeListener",
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed Event Emitter class which can act as a Base Model for all our model
|
||||
* and communication events.
|
||||
* This makes it much easier for us to distinguish between events, as we now need
|
||||
* to properly type this, so that our events are not stringly-based and prone
|
||||
* to silly typos.
|
||||
*/
|
||||
export abstract class TypedEventEmitter<Events extends string> extends EventEmitter {
|
||||
public addListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.addListener(event, listener);
|
||||
}
|
||||
|
||||
public emit(event: Events | EventEmitterEvents, ...args: any[]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
public eventNames(): (Events | EventEmitterEvents)[] {
|
||||
return super.eventNames() as Events[];
|
||||
}
|
||||
|
||||
public listenerCount(event: Events | EventEmitterEvents): number {
|
||||
return super.listenerCount(event);
|
||||
}
|
||||
|
||||
public listeners(event: Events | EventEmitterEvents): Function[] {
|
||||
return super.listeners(event);
|
||||
}
|
||||
|
||||
public off(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.off(event, listener);
|
||||
}
|
||||
|
||||
public on(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
public once(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.once(event, listener);
|
||||
}
|
||||
|
||||
public prependListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.prependListener(event, listener);
|
||||
}
|
||||
|
||||
public prependOnceListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.prependOnceListener(event, listener);
|
||||
}
|
||||
|
||||
public removeAllListeners(event?: Events | EventEmitterEvents): this {
|
||||
return super.removeAllListeners(event);
|
||||
}
|
||||
|
||||
public removeListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
return super.removeListener(event, listener);
|
||||
}
|
||||
|
||||
public rawListeners(event: Events | EventEmitterEvents): Function[] {
|
||||
return super.rawListeners(event);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class RoomHierarchy {
|
||||
* @constructor
|
||||
*/
|
||||
constructor(
|
||||
private readonly root: Room,
|
||||
public readonly root: Room,
|
||||
private readonly pageSize?: number,
|
||||
private readonly maxDepth?: number,
|
||||
private readonly suggestedOnly = false,
|
||||
@@ -61,6 +61,10 @@ export class RoomHierarchy {
|
||||
return !!this.serverSupportError || !!this.nextBatch || !this._rooms;
|
||||
}
|
||||
|
||||
public get loading(): boolean {
|
||||
return !!this.loadRequest;
|
||||
}
|
||||
|
||||
public get rooms(): IHierarchyRoom[] {
|
||||
return this._rooms;
|
||||
}
|
||||
|
||||
+1
-1
@@ -284,7 +284,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
function debuglog(...args) {
|
||||
function debuglog(...args: any[]) {
|
||||
if (DEBUG) {
|
||||
logger.log(...args);
|
||||
}
|
||||
|
||||
+9
-9
@@ -51,14 +51,14 @@ export interface IStore {
|
||||
* Set the sync token.
|
||||
* @param {string} token
|
||||
*/
|
||||
setSyncToken(token: string);
|
||||
setSyncToken(token: string): void;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Group} group
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
storeGroup(group: Group);
|
||||
storeGroup(group: Group): void;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
@@ -79,7 +79,7 @@ export interface IStore {
|
||||
* No-op.
|
||||
* @param {Room} room
|
||||
*/
|
||||
storeRoom(room: Room);
|
||||
storeRoom(room: Room): void;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
@@ -98,7 +98,7 @@ export interface IStore {
|
||||
* Permanently delete a room.
|
||||
* @param {string} roomId
|
||||
*/
|
||||
removeRoom(roomId: string);
|
||||
removeRoom(roomId: string): void;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
@@ -110,7 +110,7 @@ export interface IStore {
|
||||
* No-op.
|
||||
* @param {User} user
|
||||
*/
|
||||
storeUser(user: User);
|
||||
storeUser(user: User): void;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
@@ -140,13 +140,13 @@ export interface IStore {
|
||||
* @param {string} token The token associated with these events.
|
||||
* @param {boolean} toStart True if these are paginated results.
|
||||
*/
|
||||
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean);
|
||||
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void;
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
storeFilter(filter: Filter);
|
||||
storeFilter(filter: Filter): void;
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
@@ -168,13 +168,13 @@ export interface IStore {
|
||||
* @param {string} filterName
|
||||
* @param {string} filterId
|
||||
*/
|
||||
setFilterIdByName(filterName: string, filterId: string);
|
||||
setFilterIdByName(filterName: string, filterId: string): void;
|
||||
|
||||
/**
|
||||
* Store user-scoped account data events
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
*/
|
||||
storeAccountDataEvents(events: MatrixEvent[]);
|
||||
storeAccountDataEvents(events: MatrixEvent[]): void;
|
||||
|
||||
/**
|
||||
* Get account data event by event type
|
||||
|
||||
@@ -66,7 +66,7 @@ function selectQuery<T>(
|
||||
): Promise<T[]> {
|
||||
const query = store.openCursor(keyRange);
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
const results: T[] = [];
|
||||
query.onerror = () => {
|
||||
reject(new Error("Query failed: " + query.error));
|
||||
};
|
||||
@@ -238,7 +238,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
const range = IDBKeyRange.only(roomId);
|
||||
const request = roomIndex.openCursor(range);
|
||||
|
||||
const membershipEvents = [];
|
||||
const membershipEvents: IEvent[] = [];
|
||||
// did we encounter the oob_written marker object
|
||||
// amongst the results? That means OOB member
|
||||
// loading already happened for this room
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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 { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
export enum LocalStorageErrors {
|
||||
Global = 'Global',
|
||||
SetItemError = 'setItem',
|
||||
GetItemError = 'getItem',
|
||||
RemoveItemError = 'removeItem',
|
||||
ClearError = 'clear',
|
||||
QuotaExceededError = 'QuotaExceededError'
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible
|
||||
* As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere.
|
||||
* This store, as an event emitter, is used to re-emit local storage exceptions so that we can handle them
|
||||
* and show some kind of a "It's dead Jim" modal to the users, telling them that hey,
|
||||
* maybe you should check out your disk, as it's probably dying and your session may die with it.
|
||||
* See: https://github.com/vector-im/element-web/issues/18423
|
||||
*/
|
||||
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors> {}
|
||||
export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();
|
||||
+10
-43
@@ -285,7 +285,7 @@ export class SyncApi {
|
||||
}
|
||||
leaveObj.timeline = leaveObj.timeline || {};
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
|
||||
@@ -307,39 +307,6 @@ export class SyncApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split events between the ones that will end up in the main
|
||||
* room timeline versus the one that need to be processed in a thread
|
||||
* @experimental
|
||||
*/
|
||||
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
|
||||
if (this.opts.experimentalThreadSupport) {
|
||||
return events.reduce((memo, event: MatrixEvent) => {
|
||||
const room = this.client.getRoom(event.getRoomId());
|
||||
// An event should live in the thread timeline if
|
||||
// - It's a reply in thread event
|
||||
// - It's related to a reply in thread event
|
||||
let shouldLiveInThreadTimeline = event.replyInThread;
|
||||
if (!shouldLiveInThreadTimeline) {
|
||||
const parentEventId = event.parentEventId;
|
||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
|
||||
return mxEv.getId() === parentEventId;
|
||||
});
|
||||
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
|
||||
}
|
||||
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
|
||||
return memo;
|
||||
}, [[], []]);
|
||||
} else {
|
||||
// When `experimentalThreadSupport` is disabled
|
||||
// treat all events as timelineEvents
|
||||
return [
|
||||
events,
|
||||
[],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek into a room. This will result in the room in question being synced so it
|
||||
* is accessible via getRooms(). Live updates for the room will be provided.
|
||||
@@ -538,6 +505,7 @@ export class SyncApi {
|
||||
// The logout already happened, we just need to stop.
|
||||
logger.warn("Token no longer valid - assuming logout");
|
||||
this.stop();
|
||||
this.updateSyncState(SyncState.Error, { error });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1320,7 +1288,7 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
@@ -1388,7 +1356,7 @@ export class SyncApi {
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
@@ -1448,11 +1416,14 @@ export class SyncApi {
|
||||
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
|
||||
this.opts.crypto.updateOneTimeKeyCount(currentCount);
|
||||
}
|
||||
if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
|
||||
if (this.opts.crypto &&
|
||||
(data["device_unused_fallback_key_types"] ||
|
||||
data["org.matrix.msc2732.device_unused_fallback_key_types"])) {
|
||||
// The presence of device_unused_fallback_key_types indicates that the
|
||||
// server supports fallback keys. If there's no unused
|
||||
// signed_curve25519 fallback key we need a new one.
|
||||
const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"];
|
||||
const unusedFallbackKeys = data["device_unused_fallback_key_types"] ||
|
||||
data["org.matrix.msc2732.device_unused_fallback_key_types"];
|
||||
this.opts.crypto.setNeedsNewFallback(
|
||||
unusedFallbackKeys instanceof Array &&
|
||||
!unusedFallbackKeys.includes("signed_curve25519"),
|
||||
@@ -1734,11 +1705,7 @@ export class SyncApi {
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
threadedEvents
|
||||
.sort((a, b) => a.getTs() - b.getTs())
|
||||
.forEach(event => {
|
||||
room.addThreadedEvent(event);
|
||||
});
|
||||
return this.client.processThreadEvents(room, threadedEvents);
|
||||
}
|
||||
|
||||
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
|
||||
|
||||
@@ -99,7 +99,7 @@ export class TimelineWindow {
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
public load(initialEventId: string, initialWindowSize = 20): Promise<any> {
|
||||
public load(initialEventId?: string, initialWindowSize = 20): Promise<any> {
|
||||
// given an EventTimeline, find the event we were looking for, and initialise our
|
||||
// fields so that the event in question is in the middle of the window.
|
||||
const initFields = (timeline: EventTimeline) => {
|
||||
|
||||
+26
-34
@@ -22,16 +22,24 @@ limitations under the License.
|
||||
|
||||
import unhomoglyph from "unhomoglyph";
|
||||
import promiseRetry from "p-retry";
|
||||
|
||||
import type NodeCrypto from "crypto";
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
* Omits any undefined/null values.
|
||||
* @param {Object} params A dict of key/values to encode e.g.
|
||||
* {"foo": "bar", "baz": "taz"}
|
||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||
*/
|
||||
export function encodeParams(params: Record<string, string>): string {
|
||||
return new URLSearchParams(params).toString();
|
||||
export function encodeParams(params: Record<string, string | number | boolean>): string {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, val] of Object.entries(params)) {
|
||||
if (val !== undefined && val !== null) {
|
||||
searchParams.set(key, String(val));
|
||||
}
|
||||
}
|
||||
return searchParams.toString();
|
||||
}
|
||||
|
||||
export type QueryDict = Record<string, string | string[]>;
|
||||
@@ -90,23 +98,20 @@ export function removeElement<T>(
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean,
|
||||
) {
|
||||
let i;
|
||||
let removed;
|
||||
): boolean {
|
||||
let i: number;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return removed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return removed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,31 +281,6 @@ export function deepSortedObjectEntries(obj: any): [string, any][] {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy properties from one object to another.
|
||||
*
|
||||
* All enumerable properties, included inherited ones, are copied.
|
||||
*
|
||||
* This is approximately equivalent to ES6's Object.assign, except
|
||||
* that the latter doesn't copy inherited properties.
|
||||
*
|
||||
* @param {Object} target The object that will receive new properties
|
||||
* @param {...Object} source Objects from which to copy properties
|
||||
*
|
||||
* @return {Object} target
|
||||
*/
|
||||
export function extend(...restParams) {
|
||||
const target = restParams[0] || {};
|
||||
for (let i = 1; i < restParams.length; i++) {
|
||||
const source = restParams[i];
|
||||
if (!source) continue;
|
||||
for (const propName in source) { // eslint-disable-line guard-for-in
|
||||
target[propName] = source[propName];
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit the prototype methods from one constructor into another. This is a
|
||||
* port of the Node.js implementation with an Object.create polyfill.
|
||||
@@ -387,6 +367,18 @@ export function removeHiddenChars(str: string): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the direction override characters from a string
|
||||
* @param {string} input
|
||||
* @returns string with chars removed
|
||||
*/
|
||||
export function removeDirectionOverrideChars(str: string): string {
|
||||
if (typeof str === "string") {
|
||||
return str.replace(/[\u202d-\u202e]/g, '');
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function normalize(str: string): string {
|
||||
// Note: we have to match the filter with the removeHiddenChars() because the
|
||||
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
||||
|
||||
+193
-56
@@ -21,8 +21,9 @@ limitations under the License.
|
||||
* @module webrtc/call
|
||||
*/
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as utils from '../utils';
|
||||
import { MatrixEvent } from '../models/event';
|
||||
import { EventType } from '../@types/event';
|
||||
@@ -416,10 +417,26 @@ export class MatrixCall extends EventEmitter {
|
||||
return this.localUsermediaFeed?.stream;
|
||||
}
|
||||
|
||||
private get localScreensharingStream(): MediaStream {
|
||||
public get localScreensharingStream(): MediaStream {
|
||||
return this.localScreensharingFeed?.stream;
|
||||
}
|
||||
|
||||
public get remoteUsermediaFeed(): CallFeed {
|
||||
return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
|
||||
}
|
||||
|
||||
public get remoteScreensharingFeed(): CallFeed {
|
||||
return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
|
||||
}
|
||||
|
||||
public get remoteUsermediaStream(): MediaStream {
|
||||
return this.remoteUsermediaFeed?.stream;
|
||||
}
|
||||
|
||||
public get remoteScreensharingStream(): MediaStream {
|
||||
return this.remoteScreensharingFeed?.stream;
|
||||
}
|
||||
|
||||
private getFeedByStreamId(streamId: string): CallFeed {
|
||||
return this.getFeeds().find((feed) => feed.stream.id === streamId);
|
||||
}
|
||||
@@ -497,7 +514,15 @@ export class MatrixCall extends EventEmitter {
|
||||
if (existingFeed) {
|
||||
existingFeed.setNewStream(stream);
|
||||
} else {
|
||||
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, audioMuted, videoMuted));
|
||||
this.feeds.push(new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
userId,
|
||||
stream,
|
||||
purpose,
|
||||
audioMuted,
|
||||
videoMuted,
|
||||
}));
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
|
||||
@@ -528,50 +553,102 @@ export class MatrixCall extends EventEmitter {
|
||||
if (feed) {
|
||||
feed.setNewStream(stream);
|
||||
} else {
|
||||
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
|
||||
this.feeds.push(new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
userId,
|
||||
stream,
|
||||
purpose,
|
||||
}));
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
|
||||
logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`);
|
||||
}
|
||||
|
||||
private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
|
||||
private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
|
||||
const userId = this.client.getUserId();
|
||||
|
||||
// TODO: Find out what is going on here
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
|
||||
// We try to replace an existing feed if there already is one with the same purpose
|
||||
const existingFeed = this.getLocalFeeds().find((feed) => feed.purpose === purpose);
|
||||
if (existingFeed) {
|
||||
existingFeed.setNewStream(stream);
|
||||
} else {
|
||||
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
|
||||
this.pushLocalFeed(
|
||||
new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
userId,
|
||||
stream,
|
||||
purpose,
|
||||
}),
|
||||
addToPeerConnection,
|
||||
);
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Find out what is going on here
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
/**
|
||||
* Pushes supplied feed to the call
|
||||
* @param {CallFeed} callFeed to push
|
||||
* @param {boolean} addToPeerConnection whether to add the tracks to the peer connection
|
||||
*/
|
||||
public pushLocalFeed(callFeed: CallFeed, addToPeerConnection = true): void {
|
||||
this.feeds.push(callFeed);
|
||||
|
||||
if (addToPeerConnection) {
|
||||
const senderArray = purpose === SDPStreamMetadataPurpose.Usermedia ?
|
||||
const senderArray = callFeed.purpose === SDPStreamMetadataPurpose.Usermedia ?
|
||||
this.usermediaSenders : this.screensharingSenders;
|
||||
// Empty the array
|
||||
senderArray.splice(0, senderArray.length);
|
||||
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
for (const track of stream.getTracks()) {
|
||||
for (const track of callFeed.stream.getTracks()) {
|
||||
logger.info(
|
||||
`Adding track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${purpose}"` +
|
||||
`streamId="${callFeed.stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
senderArray.push(this.peerConn.addTrack(track, stream));
|
||||
senderArray.push(this.peerConn.addTrack(track, callFeed.stream));
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`);
|
||||
logger.info(
|
||||
`Pushed local stream ` +
|
||||
`(id="${callFeed.stream.id}", ` +
|
||||
`active="${callFeed.stream.active}", ` +
|
||||
`purpose="${callFeed.purpose}")`,
|
||||
);
|
||||
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes local call feed from the call and its tracks from the peer
|
||||
* connection
|
||||
* @param callFeed to remove
|
||||
*/
|
||||
public removeLocalFeed(callFeed: CallFeed): void {
|
||||
const senderArray = callFeed.purpose === SDPStreamMetadataPurpose.Usermedia
|
||||
? this.usermediaSenders
|
||||
: this.screensharingSenders;
|
||||
|
||||
for (const sender of senderArray) {
|
||||
this.peerConn.removeTrack(sender);
|
||||
}
|
||||
// Empty the array
|
||||
senderArray.splice(0, senderArray.length);
|
||||
this.deleteFeedByStream(callFeed.stream);
|
||||
}
|
||||
|
||||
private deleteAllFeeds(): void {
|
||||
@@ -728,25 +805,60 @@ export class MatrixCall extends EventEmitter {
|
||||
logger.debug(`Answering call ${this.callId}`);
|
||||
|
||||
if (!this.localUsermediaStream && !this.waitForLocalAVStream) {
|
||||
const prevState = this.state;
|
||||
const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio");
|
||||
const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video");
|
||||
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
this.waitForLocalAVStream = true;
|
||||
|
||||
try {
|
||||
const mediaStream = await this.client.getMediaHandler().getUserMediaStream(
|
||||
this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio"),
|
||||
this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video"),
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(
|
||||
answerWithAudio, answerWithVideo,
|
||||
);
|
||||
this.waitForLocalAVStream = false;
|
||||
this.gotUserMediaForAnswer(mediaStream);
|
||||
const usermediaFeed = new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
userId: this.client.getUserId(),
|
||||
stream,
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
});
|
||||
|
||||
const feeds = [usermediaFeed];
|
||||
|
||||
if (this.localScreensharingFeed) {
|
||||
feeds.push(this.localScreensharingFeed);
|
||||
}
|
||||
|
||||
this.answerWithCallFeeds(feeds);
|
||||
} catch (e) {
|
||||
this.getUserMediaFailed(e);
|
||||
return;
|
||||
if (answerWithVideo) {
|
||||
// Try to answer without video
|
||||
logger.warn("Failed to getUserMedia(), trying to getUserMedia() without video");
|
||||
this.setState(prevState);
|
||||
this.waitForLocalAVStream = false;
|
||||
await this.answer(answerWithAudio, false);
|
||||
} else {
|
||||
this.getUserMediaFailed(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (this.waitForLocalAVStream) {
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
}
|
||||
}
|
||||
|
||||
public answerWithCallFeeds(callFeeds: CallFeed[]): void {
|
||||
if (this.inviteOrAnswerSent) return;
|
||||
|
||||
logger.debug(`Answering call ${this.callId}`);
|
||||
|
||||
this.gotCallFeedsForAnswer(callFeeds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this call with a new call, e.g. for glare resolution. Used by
|
||||
* MatrixClient.
|
||||
@@ -758,7 +870,7 @@ export class MatrixCall extends EventEmitter {
|
||||
newCall.waitForLocalAVStream = true;
|
||||
} else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) {
|
||||
logger.debug("Handing local stream to new call");
|
||||
newCall.gotUserMediaForAnswer(this.localUsermediaStream);
|
||||
newCall.gotCallFeedsForAnswer(this.getLocalFeeds());
|
||||
}
|
||||
this.successor = newCall;
|
||||
this.emit(CallEvent.Replaced, newCall);
|
||||
@@ -830,7 +942,7 @@ export class MatrixCall extends EventEmitter {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
|
||||
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
} else if (upgradeAudio) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
|
||||
@@ -896,12 +1008,10 @@ export class MatrixCall extends EventEmitter {
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
|
||||
if (!stream) return false;
|
||||
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(CallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", err),
|
||||
);
|
||||
logger.error("Failed to get screen-sharing stream:", err);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -938,13 +1048,11 @@ export class MatrixCall extends EventEmitter {
|
||||
});
|
||||
sender.replaceTrack(track);
|
||||
|
||||
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.emit(CallEvent.Error,
|
||||
new CallError(CallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", err),
|
||||
);
|
||||
logger.error("Failed to get screen-sharing stream:", err);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -969,6 +1077,10 @@ export class MatrixCall extends EventEmitter {
|
||||
* @returns the new mute state
|
||||
*/
|
||||
public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
|
||||
if (!await this.client.getMediaHandler().hasVideoDevice()) {
|
||||
return this.isLocalVideoMuted();
|
||||
}
|
||||
|
||||
if (!this.hasLocalUserMediaVideoTrack && !muted) {
|
||||
await this.upgradeCall(false, true);
|
||||
return this.isLocalVideoMuted();
|
||||
@@ -997,6 +1109,10 @@ export class MatrixCall extends EventEmitter {
|
||||
* @returns the new mute state
|
||||
*/
|
||||
public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
|
||||
if (!await this.client.getMediaHandler().hasAudioDevice()) {
|
||||
return this.isMicrophoneMuted();
|
||||
}
|
||||
|
||||
if (!this.hasLocalUserMediaAudioTrack && !muted) {
|
||||
await this.upgradeCall(true, false);
|
||||
return this.isMicrophoneMuted();
|
||||
@@ -1090,13 +1206,9 @@ export class MatrixCall extends EventEmitter {
|
||||
setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal
|
||||
* @param {Object} stream
|
||||
*/
|
||||
private gotUserMediaForInvite = async (stream: MediaStream): Promise<void> => {
|
||||
private gotCallFeedsForInvite(callFeeds: CallFeed[]): void {
|
||||
if (this.successor) {
|
||||
this.successor.gotUserMediaForAnswer(stream);
|
||||
this.successor.gotCallFeedsForAnswer(callFeeds);
|
||||
return;
|
||||
}
|
||||
if (this.callHasEnded()) {
|
||||
@@ -1104,12 +1216,15 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
for (const feed of callFeeds) {
|
||||
this.pushLocalFeed(feed);
|
||||
}
|
||||
|
||||
this.setState(CallState.CreateOffer);
|
||||
|
||||
logger.debug("gotUserMediaForInvite");
|
||||
// Now we wait for the negotiationneeded event
|
||||
};
|
||||
}
|
||||
|
||||
private async sendAnswer(): Promise<void> {
|
||||
const answerContent = {
|
||||
@@ -1158,12 +1273,15 @@ export class MatrixCall extends EventEmitter {
|
||||
this.sendCandidateQueue();
|
||||
}
|
||||
|
||||
private gotUserMediaForAnswer = async (stream: MediaStream): Promise<void> => {
|
||||
if (this.callHasEnded()) {
|
||||
return;
|
||||
private async gotCallFeedsForAnswer(callFeeds: CallFeed[]): Promise<void> {
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
this.waitForLocalAVStream = false;
|
||||
|
||||
for (const feed of callFeeds) {
|
||||
this.pushLocalFeed(feed);
|
||||
}
|
||||
|
||||
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
this.setState(CallState.CreateAnswer);
|
||||
|
||||
let myAnswer;
|
||||
@@ -1191,7 +1309,7 @@ export class MatrixCall extends EventEmitter {
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal
|
||||
@@ -1933,15 +2051,41 @@ export class MatrixCall extends EventEmitter {
|
||||
* @throws if have passed audio=false.
|
||||
*/
|
||||
public async placeCall(audio: boolean, video: boolean): Promise<void> {
|
||||
logger.debug(`placeCall audio=${audio} video=${video}`);
|
||||
if (!audio) {
|
||||
throw new Error("You CANNOT start a call without audio");
|
||||
}
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
|
||||
const callFeed = new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
userId: this.client.getUserId(),
|
||||
stream,
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
});
|
||||
await this.placeCallWithCallFeeds([callFeed]);
|
||||
} catch (e) {
|
||||
this.getUserMediaFailed(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a call to this room with call feed.
|
||||
* @param {CallFeed[]} callFeeds to use
|
||||
* @throws if you have not specified a listener for 'error' events.
|
||||
* @throws if have passed audio=false.
|
||||
*/
|
||||
public async placeCallWithCallFeeds(callFeeds: CallFeed[]): Promise<void> {
|
||||
this.checkForErrorListener();
|
||||
this.direction = CallDirection.Outbound;
|
||||
|
||||
// XXX Find a better way to do this
|
||||
this.client.callEventHandler.calls.set(this.callId, this);
|
||||
this.setState(CallState.WaitLocalMedia);
|
||||
this.direction = CallDirection.Outbound;
|
||||
|
||||
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
||||
// poll and keep the credentials valid so this should be instant.
|
||||
@@ -1953,14 +2097,7 @@ export class MatrixCall extends EventEmitter {
|
||||
// create the peer connection now so it can be gathering candidates while we get user
|
||||
// media (assuming a candidate pool size is configured)
|
||||
this.peerConn = this.createPeerConnection();
|
||||
|
||||
try {
|
||||
const mediaStream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
|
||||
this.gotUserMediaForInvite(mediaStream);
|
||||
} catch (e) {
|
||||
this.getUserMediaFailed(e);
|
||||
return;
|
||||
}
|
||||
this.gotCallFeedsForInvite(callFeeds);
|
||||
}
|
||||
|
||||
private createPeerConnection(): RTCPeerConnection {
|
||||
|
||||
+77
-29
@@ -15,12 +15,30 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { MatrixClient } from "../client";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
|
||||
const POLLING_INTERVAL = 250; // ms
|
||||
const SPEAKING_THRESHOLD = -60; // dB
|
||||
const POLLING_INTERVAL = 200; // ms
|
||||
export const SPEAKING_THRESHOLD = -60; // dB
|
||||
const SPEAKING_SAMPLE_COUNT = 8; // samples
|
||||
|
||||
export interface ICallFeedOpts {
|
||||
client: MatrixClient;
|
||||
roomId: string;
|
||||
userId: string;
|
||||
stream: MediaStream;
|
||||
purpose: SDPStreamMetadataPurpose;
|
||||
/**
|
||||
* Whether or not the remote SDPStreamMetadata says audio is muted
|
||||
*/
|
||||
audioMuted: boolean;
|
||||
/**
|
||||
* Whether or not the remote SDPStreamMetadata says video is muted
|
||||
*/
|
||||
videoMuted: boolean;
|
||||
}
|
||||
|
||||
export enum CallFeedEvent {
|
||||
NewStream = "new_stream",
|
||||
@@ -30,6 +48,15 @@ export enum CallFeedEvent {
|
||||
}
|
||||
|
||||
export class CallFeed extends EventEmitter {
|
||||
public stream: MediaStream;
|
||||
public userId: string;
|
||||
public purpose: SDPStreamMetadataPurpose;
|
||||
public speakingVolumeSamples: number[];
|
||||
|
||||
private client: MatrixClient;
|
||||
private roomId: string;
|
||||
private audioMuted: boolean;
|
||||
private videoMuted: boolean;
|
||||
private measuringVolumeActivity = false;
|
||||
private audioContext: AudioContext;
|
||||
private analyser: AnalyserNode;
|
||||
@@ -38,17 +65,18 @@ export class CallFeed extends EventEmitter {
|
||||
private speaking = false;
|
||||
private volumeLooperTimeout: number;
|
||||
|
||||
constructor(
|
||||
public stream: MediaStream,
|
||||
public userId: string,
|
||||
public purpose: SDPStreamMetadataPurpose,
|
||||
private client: MatrixClient,
|
||||
private roomId: string,
|
||||
private audioMuted: boolean,
|
||||
private videoMuted: boolean,
|
||||
) {
|
||||
constructor(opts: ICallFeedOpts) {
|
||||
super();
|
||||
this.updateStream(null, stream);
|
||||
|
||||
this.client = opts.client;
|
||||
this.roomId = opts.roomId;
|
||||
this.userId = opts.userId;
|
||||
this.purpose = opts.purpose;
|
||||
this.audioMuted = opts.audioMuted;
|
||||
this.videoMuted = opts.videoMuted;
|
||||
this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity);
|
||||
|
||||
this.updateStream(null, opts.stream);
|
||||
|
||||
if (this.hasAudioTrack) {
|
||||
this.initVolumeMeasuring();
|
||||
@@ -60,6 +88,8 @@ export class CallFeed extends EventEmitter {
|
||||
}
|
||||
|
||||
private updateStream(oldStream: MediaStream, newStream: MediaStream): void {
|
||||
if (newStream === oldStream) return;
|
||||
|
||||
if (oldStream) {
|
||||
oldStream.removeEventListener("addtrack", this.onAddTrack);
|
||||
this.measureVolumeActivity(false);
|
||||
@@ -134,6 +164,10 @@ export class CallFeed extends EventEmitter {
|
||||
return this.stream.getVideoTracks().length === 0 || this.videoMuted;
|
||||
}
|
||||
|
||||
public isSpeaking(): boolean {
|
||||
return this.speaking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current MediaStream with a new one.
|
||||
* This method should be only used by MatrixCall.
|
||||
@@ -149,6 +183,7 @@ export class CallFeed extends EventEmitter {
|
||||
*/
|
||||
public setAudioMuted(muted: boolean): void {
|
||||
this.audioMuted = muted;
|
||||
this.speakingVolumeSamples.fill(-Infinity);
|
||||
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
|
||||
}
|
||||
|
||||
@@ -173,6 +208,7 @@ export class CallFeed extends EventEmitter {
|
||||
this.volumeLooper();
|
||||
} else {
|
||||
this.measuringVolumeActivity = false;
|
||||
this.speakingVolumeSamples.fill(-Infinity);
|
||||
this.emit(CallFeedEvent.VolumeChanged, -Infinity);
|
||||
}
|
||||
}
|
||||
@@ -181,31 +217,43 @@ export class CallFeed extends EventEmitter {
|
||||
this.speakingThreshold = threshold;
|
||||
}
|
||||
|
||||
private volumeLooper(): void {
|
||||
private volumeLooper = () => {
|
||||
if (!this.analyser) return;
|
||||
|
||||
this.volumeLooperTimeout = setTimeout(() => {
|
||||
if (!this.measuringVolumeActivity) return;
|
||||
if (!this.measuringVolumeActivity) return;
|
||||
|
||||
this.analyser.getFloatFrequencyData(this.frequencyBinCount);
|
||||
this.analyser.getFloatFrequencyData(this.frequencyBinCount);
|
||||
|
||||
let maxVolume = -Infinity;
|
||||
for (let i = 0; i < this.frequencyBinCount.length; i++) {
|
||||
if (this.frequencyBinCount[i] > maxVolume) {
|
||||
maxVolume = this.frequencyBinCount[i];
|
||||
}
|
||||
let maxVolume = -Infinity;
|
||||
for (let i = 0; i < this.frequencyBinCount.length; i++) {
|
||||
if (this.frequencyBinCount[i] > maxVolume) {
|
||||
maxVolume = this.frequencyBinCount[i];
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(CallFeedEvent.VolumeChanged, maxVolume);
|
||||
const newSpeaking = maxVolume > this.speakingThreshold;
|
||||
if (this.speaking !== newSpeaking) {
|
||||
this.speaking = newSpeaking;
|
||||
this.emit(CallFeedEvent.Speaking, this.speaking);
|
||||
this.speakingVolumeSamples.shift();
|
||||
this.speakingVolumeSamples.push(maxVolume);
|
||||
|
||||
this.emit(CallFeedEvent.VolumeChanged, maxVolume);
|
||||
|
||||
let newSpeaking = false;
|
||||
|
||||
for (let i = 0; i < this.speakingVolumeSamples.length; i++) {
|
||||
const volume = this.speakingVolumeSamples[i];
|
||||
|
||||
if (volume > this.speakingThreshold) {
|
||||
newSpeaking = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.volumeLooper();
|
||||
}, POLLING_INTERVAL);
|
||||
}
|
||||
if (this.speaking !== newSpeaking) {
|
||||
this.speaking = newSpeaking;
|
||||
this.emit(CallFeedEvent.Speaking, this.speaking);
|
||||
}
|
||||
|
||||
this.volumeLooperTimeout = setTimeout(this.volumeLooper, POLLING_INTERVAL);
|
||||
};
|
||||
|
||||
public dispose(): void {
|
||||
clearTimeout(this.volumeLooperTimeout);
|
||||
|
||||
@@ -43,23 +43,37 @@ export class MediaHandler {
|
||||
this.videoInput = deviceId;
|
||||
}
|
||||
|
||||
public async hasAudioDevice(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter(device => device.kind === "audioinput").length > 0;
|
||||
}
|
||||
|
||||
public async hasVideoDevice(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter(device => device.kind === "videoinput").length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
|
||||
const shouldRequestAudio = audio && await this.hasAudioDevice();
|
||||
const shouldRequestVideo = video && await this.hasVideoDevice();
|
||||
|
||||
let stream: MediaStream;
|
||||
|
||||
// Find a stream with matching tracks
|
||||
const matchingStream = this.userMediaStreams.find((stream) => {
|
||||
if (audio !== (stream.getAudioTracks().length > 0)) return false;
|
||||
if (video !== (stream.getVideoTracks().length > 0)) return false;
|
||||
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
|
||||
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingStream) {
|
||||
logger.log("Cloning user media stream", matchingStream.id);
|
||||
stream = matchingStream.clone();
|
||||
} else {
|
||||
const constraints = this.getUserMediaContraints(audio, video);
|
||||
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
Reference in New Issue
Block a user