Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| f0597e0124 | |||
| 2fdc91bd04 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 | |||
| 83d1a6c046 | |||
| 87393c7db8 | |||
| bd47667e63 | |||
| cfd865bf8b | |||
| 9b54df7b2b | |||
| 5da562fa6f | |||
| 83d952eae8 | |||
| f30be87879 | |||
| ab19480cc5 | |||
| 99451698a4 | |||
| 48dbed8001 | |||
| bae883a891 | |||
| 2aae2362e3 | |||
| f780e1dbc3 | |||
| fdc11b5e53 | |||
| 1a0bdb0f98 | |||
| e870bebd3d | |||
| dd0ca91aa9 | |||
| 36906e2ddb | |||
| 3eaed30446 | |||
| 72643026a3 | |||
| d6980cb8fa | |||
| dcd24fd516 | |||
| 75cc873d77 | |||
| dd23a1a401 | |||
| 6ac84a2465 | |||
| e4d3124e72 | |||
| df11a9e832 | |||
| e4703989fe | |||
| feb83ba161 | |||
| 22a98b3e4f | |||
| 934a0e8943 | |||
| 963c7690b6 | |||
| 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 | |||
| 8f245af317 | |||
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 7559a10526 | |||
| 68dbe959bb | |||
| 74a875aa23 | |||
| 4b679c5056 | |||
| cb8a260b9c | |||
| 7d7975b8bb | |||
| f12513da96 | |||
| 785a3e3381 | |||
| 29bc83f4a7 | |||
| 13c8774d45 | |||
| c9212e770d | |||
| 08aa7803e3 | |||
| 770858c255 | |||
| c73bb7d408 | |||
| c63abe9988 | |||
| 482e3c4cb7 | |||
| 3c97d17770 |
+207
-2
@@ -1,3 +1,210 @@
|
||||
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
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)
|
||||
===================================================================================================
|
||||
|
||||
@@ -16,8 +223,6 @@ Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v1
|
||||
* 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.2](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.2) (2021-10-07)
|
||||
=============================================================================================================
|
||||
|
||||
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
|
||||
=============================================================================================================
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
console.log("Loading browser sdk");
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
var USER_ID = "@username:localhost";
|
||||
var ROOM_ID = "!room:id";
|
||||
const BASE_URL = "https://matrix.org";
|
||||
const TOKEN = "accesstokengoeshere";
|
||||
const USER_ID = "@username:localhost";
|
||||
const ROOM_ID = "!room:id";
|
||||
const DEVICE_ID = "some_device_id";
|
||||
|
||||
|
||||
var client = matrixcs.createClient({
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
});
|
||||
var call;
|
||||
let call;
|
||||
|
||||
function disableButtons(place, answer, hangup) {
|
||||
document.getElementById("hangup").disabled = hangup;
|
||||
@@ -19,7 +20,7 @@ function disableButtons(place, answer, hangup) {
|
||||
}
|
||||
|
||||
function addListeners(call) {
|
||||
var lastError = "";
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
|
||||
+11
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "14.0.1",
|
||||
"version": "15.5.1",
|
||||
"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 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
@@ -56,6 +56,7 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.6",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
@@ -74,14 +75,15 @@
|
||||
"@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/content-type": "^1.1.5",
|
||||
"@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 +91,8 @@
|
||||
"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-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
@@ -99,7 +102,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
|
||||
@@ -255,6 +255,12 @@ if [ -n "$signing_id" ]; then
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# This uses git archive which seems to be what github uses. Specifically,
|
||||
# the header fields are set in the same way: same file mode, uid & gid
|
||||
# both zero and mtime set to the timestamp of the commit that the tag
|
||||
# references. Also note that this puts the commit into the tar headers
|
||||
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -85,6 +86,7 @@ TestClient.prototype.toString = function() {
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
@@ -17,54 +17,34 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
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";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
/* global matrixcs */
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
|
||||
async function createTestClient() {
|
||||
const sessionStoreBackend = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
const httpBackend = new MockHttpBackend();
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
const options = {
|
||||
baseUrl: "http://" + USER_ID + ".test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
sessionStore: sessionStore,
|
||||
request: httpBackend.requestFn,
|
||||
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
|
||||
};
|
||||
|
||||
const client = matrixcs.createClient(options);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
return { client, httpBackend };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({ client, httpBackend } = await createTestClient());
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
await httpBackend.stop();
|
||||
httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async function() {
|
||||
@@ -91,10 +71,8 @@ describe("Browserify Test", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
]),
|
||||
return await Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
|
||||
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
@@ -722,6 +722,7 @@ describe("MatrixClient crypto", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
logger.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("MatrixClient events", function() {
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -71,6 +71,7 @@ const EVENTS = [
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
@@ -502,7 +503,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
@@ -553,7 +554,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
expect(params.limit).toEqual("20");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
let client = null;
|
||||
@@ -392,6 +394,492 @@ describe("MatrixClient", function() {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionThreadedEvents", function() {
|
||||
it("returns empty arrays when given an empty arrays", function() {
|
||||
const events = [];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
expect(timeline).toEqual([]);
|
||||
expect(threaded).toEqual([]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
// Vote has no threadId yet
|
||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
// The vote event has been copied into the thread
|
||||
const eventRefWithThreadId = withThreadId(
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId());
|
||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
eventRefWithThreadId,
|
||||
// Thread does not see thread root
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventPollResponseReference,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-thread vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
// Events for this test only, because we hack around with them
|
||||
const eventMessageInThread2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot2 = new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollResponseReference2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
// When we react within a thread, sometimes the thread root
|
||||
// has isThreadRelation === true, because thread is set on it,
|
||||
// but threadId is not.
|
||||
eventPollStartThreadRoot2.setThread(
|
||||
new Thread(
|
||||
eventPollStartThreadRoot2,
|
||||
{
|
||||
client,
|
||||
room: new Room(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference2,
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference2,
|
||||
// eventPollStartThreadRoot2,
|
||||
// This is weird: by hacking the thread root to have an inconsistency
|
||||
// between thread and threadId (which is what I have observed in the
|
||||
// wild), we have persuaded the code that the thread root is actually
|
||||
// within the thread, so it is not provided to the main timeline.
|
||||
//
|
||||
// This should go away when we fix this inconsistency. When that
|
||||
// happens, we should probably delete this test.
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(
|
||||
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
|
||||
),
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2, // See note above for why this appears here.
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventReaction,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends room state events to the main timeline only", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// This is based on recording the events in a real room:
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
]);
|
||||
|
||||
// Thread should contain only stuff that happened in the thread -
|
||||
// no thread root, and no room state events
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event, newThreadId) {
|
||||
const ret = event.toSnapshot();
|
||||
ret.setThreadId(newThreadId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const eventMessageInThread = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollResponseReference = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventReaction = new MatrixEvent({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"key": "🤗",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
"origin_server_ts": 1643977249238,
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 22598,
|
||||
"transaction_id": "m1643977249073.16",
|
||||
},
|
||||
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot = new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventRoomName = new MatrixEvent({
|
||||
"age": 80123249,
|
||||
"content": {
|
||||
"name": "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
|
||||
"origin_server_ts": 1643815441638,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": { "age": 80123249 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventEncryption = new MatrixEvent({
|
||||
"age": 80123383,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
|
||||
"origin_server_ts": 1643815441504,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.encryption",
|
||||
"unsigned": { "age": 80123383 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventGuestAccess = new MatrixEvent({
|
||||
"age": 80123473,
|
||||
"content": {
|
||||
"guest_access": "can_join",
|
||||
},
|
||||
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
|
||||
"origin_server_ts": 1643815441414,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.guest_access",
|
||||
"unsigned": { "age": 80123473 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventHistoryVisibility = new MatrixEvent({
|
||||
"age": 80123556,
|
||||
"content": {
|
||||
"history_visibility": "shared",
|
||||
},
|
||||
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
|
||||
"origin_server_ts": 1643815441331,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": { "age": 80123556 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventJoinRules = new MatrixEvent({
|
||||
"age": 80123696,
|
||||
"content": {
|
||||
"join_rule": "invite",
|
||||
},
|
||||
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
"origin_server_ts": 1643815441191,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": { "age": 80123696 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPowerLevels = new MatrixEvent({
|
||||
"age": 80124105,
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
"events_default": 0,
|
||||
"historical": 100,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@andybalaam-test1:matrix.org": 100,
|
||||
},
|
||||
"users_default": 0,
|
||||
},
|
||||
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
|
||||
"origin_server_ts": 1643815440782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": { "age": 80124105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventMember = new MatrixEvent({
|
||||
"age": 80125279,
|
||||
"content": {
|
||||
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
"displayname": "andybalaam-test1",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
"origin_server_ts": 1643815439608,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": { "age": 80125279 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventCreate = new MatrixEvent({
|
||||
"age": 80126105,
|
||||
"content": {
|
||||
"creator": "@andybalaam-test1:matrix.org",
|
||||
"room_version": "6",
|
||||
},
|
||||
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
|
||||
"origin_server_ts": 1643815438782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": { "age": 80126105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
|
||||
@@ -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";
|
||||
@@ -104,10 +105,12 @@ describe("MatrixClient opts", function() {
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
});
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
await httpBackend.flush("/capabilities", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
|
||||
+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();
|
||||
@@ -96,7 +96,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@@ -109,6 +109,7 @@ describe("MatrixClient room timelines", function() {
|
||||
client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
@@ -116,9 +117,10 @@ describe("MatrixClient room timelines", function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
return httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
await httpBackend.flush("/capabilities");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("MatrixClient syncing", function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
@@ -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
-14
@@ -341,8 +341,15 @@ HttpResponse.SYNC_RESPONSE = {
|
||||
data: HttpResponse.SYNC_DATA,
|
||||
};
|
||||
|
||||
HttpResponse.CAPABILITIES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/capabilities",
|
||||
data: { capabilities: {} },
|
||||
};
|
||||
|
||||
HttpResponse.defaultResponses = function(userId) {
|
||||
return [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
HttpResponse.filterResponse(userId),
|
||||
HttpResponse.SYNC_RESPONSE,
|
||||
@@ -350,19 +357,13 @@ HttpResponse.defaultResponses = function(userId) {
|
||||
};
|
||||
|
||||
export function setHttpResponses(
|
||||
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
httpBackend, responses,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(
|
||||
responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client.http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
responses.forEach(response => {
|
||||
httpBackend
|
||||
.when(response.method, response.path)
|
||||
.respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
||||
|
||||
@@ -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';
|
||||
@@ -39,13 +40,14 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
const client = (new TestClient(
|
||||
const testClient = new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
);
|
||||
const client = testClient.client;
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
return client;
|
||||
return { client, httpBackend: testClient.httpBackend };
|
||||
}
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
@@ -59,7 +61,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||
@@ -79,7 +81,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
@@ -130,7 +132,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -160,7 +162,7 @@ describe("Cross Signing", function() {
|
||||
await promise;
|
||||
});
|
||||
|
||||
it("should get cross-signing keys from sync", async function() {
|
||||
it.skip("should get cross-signing keys from sync", async function() {
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
@@ -174,7 +176,7 @@ describe("Cross Signing", function() {
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
@@ -235,6 +237,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
const responses = [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -310,9 +313,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses, true, true);
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
await alice.startClient();
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
@@ -331,7 +335,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -414,9 +418,9 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
aliceKeys,
|
||||
@@ -490,6 +494,7 @@ describe("Cross Signing", function() {
|
||||
// - master key signed by her usk (pretend that it was signed by another
|
||||
// of Alice's devices)
|
||||
const responses = [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -560,10 +565,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses);
|
||||
|
||||
await alice.startClient();
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
await keyChangePromise;
|
||||
|
||||
// Bob's device key should be trusted
|
||||
@@ -578,7 +583,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -647,7 +652,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -785,7 +790,7 @@ describe("Cross Signing", function() {
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
@@ -797,7 +802,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
const { client: bob } = await makeTestClient(
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2022 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 { makeLocationContent } from "../../src/content-helpers";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Location", function() {
|
||||
it("should create a valid location with defaults", function() {
|
||||
const loc = makeLocationContent("txt", "geo:foo", 134235435);
|
||||
expect(loc.body).toEqual("txt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:foo");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: ASSET_TYPE_SELF });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
"txxt", "geo:bar", 134235436, "desc", "m.something");
|
||||
|
||||
expect(loc.body).toEqual("txxt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:bar");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: "m.something" });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
import * as testUtils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -52,6 +53,12 @@ describe("MatrixClient", function() {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
const CAPABILITIES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/capabilities",
|
||||
data: { capabilities: {} },
|
||||
};
|
||||
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
@@ -167,6 +174,7 @@ describe("MatrixClient", function() {
|
||||
acceptKeepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -364,9 +372,11 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups = [
|
||||
CAPABILITIES_RESPONSE,
|
||||
PUSH_RULES_RESPONSE,
|
||||
SYNC_RESPONSE,
|
||||
];
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
@@ -447,12 +457,15 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
await client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
@@ -503,6 +516,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
@@ -559,6 +573,7 @@ describe("MatrixClient", function() {
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
@@ -573,6 +588,7 @@ describe("MatrixClient", function() {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
@@ -697,7 +713,8 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filter
|
||||
httpLookups = []; // no /pushrules or /filterw
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -728,4 +745,207 @@ describe("MatrixClient", function() {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, "$threadId:server", EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
addPendingEvent: jest.fn(),
|
||||
updatePendingEvent: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
});
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, null, eventId, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, "$threadId:server", eventId, txnId);
|
||||
});
|
||||
|
||||
it("does not get wrongly encrypted", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const reason = "This is the redaction reason";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
expectBody: { reason }, // NOT ENCRYPTED
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelPendingEvent", () => {
|
||||
const roomId = "!room:server";
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
updatePendingEvent: (event, status) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let event;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
event_id: "~" + roomId + ":" + txnId,
|
||||
user_id: client.credentials.userId,
|
||||
sender: client.credentials.userId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
});
|
||||
event.setTxnId(txnId);
|
||||
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = { // mock crypto
|
||||
encryptEvent: (event, room) => new Promise(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
expect(event.status).toBe(EventStatus.CANCELLED);
|
||||
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
|
||||
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
|
||||
}
|
||||
|
||||
it("should cancel an event which is queued", () => {
|
||||
event.setStatus(EventStatus.QUEUED);
|
||||
client.scheduler.queueEvent(event);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
client.encryptAndSendEvent(null, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should error when given any other event status", () => {
|
||||
event.setStatus(EventStatus.SENDING);
|
||||
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
|
||||
expect(event.status).toBe(EventStatus.SENDING);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
@@ -313,7 +312,7 @@ describe("MSC3089Branch", () => {
|
||||
} as MatrixEvent);
|
||||
|
||||
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
|
||||
replacingEventId: () => null,
|
||||
replacingEventId: (): string => null,
|
||||
getId: () => "$unknown",
|
||||
}];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
|
||||
+16
-3
@@ -622,6 +622,11 @@ describe("Room", function() {
|
||||
}, event: true,
|
||||
})]);
|
||||
};
|
||||
const setAlias = function(alias) {
|
||||
room.addLiveEvents([utils.mkEvent({
|
||||
type: "m.room.canonical_alias", room: roomId, skey: "", content: { alias }, event: true,
|
||||
})]);
|
||||
};
|
||||
const setRoomName = function(name) {
|
||||
room.addLiveEvents([utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, content: {
|
||||
@@ -857,7 +862,7 @@ describe("Room", function() {
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAltAliases([alias, "#another:here"]);
|
||||
setAlias(alias);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -867,12 +872,20 @@ describe("Room", function() {
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAltAliases([alias, "#another:here"]);
|
||||
setAlias(alias);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
});
|
||||
|
||||
it("should not show alt aliases if a room name does not exist", () => {
|
||||
const alias = "#room_alias:here";
|
||||
setAltAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).not.toEqual(alias);
|
||||
});
|
||||
|
||||
it("should show the room name if one exists for private " +
|
||||
"(invite join_rules) rooms.", function() {
|
||||
const roomName = "A mighty name indeed";
|
||||
@@ -1448,7 +1461,7 @@ describe("Room", function() {
|
||||
describe("maySendMessage", function() {
|
||||
it("should return false if synced membership not join",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const room = new Room(roomId, { isRoomEncrypted: () => false }, userA);
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.updateMyMembership("leave");
|
||||
|
||||
+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;
|
||||
|
||||
+18
-4
@@ -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,10 +177,15 @@ 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",
|
||||
);
|
||||
/**
|
||||
* A type of message that affects visibility of a message,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||
"m.visibility",
|
||||
"org.matrix.msc3531.visibility");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
|
||||
@@ -14,13 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Merge this with sync.js once converted
|
||||
// Types for MSC1767: Extensible events in Matrix
|
||||
|
||||
export enum SyncState {
|
||||
Error = "ERROR",
|
||||
Prepared = "PREPARED",
|
||||
Stopped = "STOPPED",
|
||||
Syncing = "SYNCING",
|
||||
Catchup = "CATCHUP",
|
||||
Reconnecting = "RECONNECTING",
|
||||
}
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
export const TEXT_NODE_TYPE = new UnstableValue("m.text", "org.matrix.msc1767.text");
|
||||
Vendored
-6
@@ -62,12 +62,6 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Types for MSC3488 - m.location: Extending events with location data
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IContent } from "../models/event";
|
||||
import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
|
||||
export const LOCATION_EVENT_TYPE = new UnstableValue(
|
||||
"m.location", "org.matrix.msc3488.location");
|
||||
|
||||
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
|
||||
|
||||
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
|
||||
|
||||
export const ASSET_TYPE_SELF = "m.self";
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"content": {
|
||||
"body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"msgtype": "m.location",
|
||||
"geo_uri": "geo:51.5008,0.1247;u=35",
|
||||
"m.location": {
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Matthew's whereabouts",
|
||||
},
|
||||
"m.asset": {
|
||||
"type": "m.self"
|
||||
},
|
||||
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"m.ts": 1636829458432,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ILocationContent extends IContent {
|
||||
body: string;
|
||||
msgtype: string;
|
||||
geo_uri: string;
|
||||
[LOCATION_EVENT_TYPE.name]: {
|
||||
uri: string;
|
||||
description?: string;
|
||||
};
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: string;
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
[TIMESTAMP_NODE_TYPE.name]: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
@@ -82,3 +82,12 @@ export enum HistoryVisibility {
|
||||
Shared = "shared",
|
||||
WorldReadable = "world_readable",
|
||||
}
|
||||
|
||||
export interface IUsageLimit {
|
||||
// "hs_disabled" is NOT a specced string, but is used in Synapse
|
||||
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
|
||||
// eslint-disable-next-line camelcase
|
||||
limit_type: "monthly_active_user" | "hs_disabled" | string;
|
||||
// eslint-disable-next-line camelcase
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
+17
-1
@@ -15,10 +15,11 @@ 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";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@@ -139,4 +140,19 @@ export interface IBindThreePidBody {
|
||||
id_access_token: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
original_event: IEvent;
|
||||
chunk: IEvent[];
|
||||
next_batch?: string;
|
||||
prev_batch?: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -44,7 +44,6 @@ export interface ISpaceSummaryEvent {
|
||||
}
|
||||
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
room_id: string;
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
order?: string;
|
||||
|
||||
+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
|
||||
|
||||
+1009
-358
File diff suppressed because it is too large
Load Diff
+42
-2
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2022 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.
|
||||
@@ -18,6 +17,14 @@ limitations under the License.
|
||||
/** @module ContentHelpers */
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
ILocationContent,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "./@types/location";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -99,3 +106,36 @@ export function makeEmoteMessage(body: string) {
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Location event
|
||||
* @param text a text for of our location
|
||||
* @param uri a geo:// uri for the location
|
||||
* @param ts the timestamp when the location was correct (milliseconds since
|
||||
* the UNIX epoch)
|
||||
* @param description the (optional) label for this location on the map
|
||||
* @param asset_type the (optional) asset type of this location e.g. "m.self"
|
||||
*/
|
||||
export function makeLocationContent(
|
||||
text: string,
|
||||
uri: string,
|
||||
ts: number,
|
||||
description?: string,
|
||||
assetType?: string,
|
||||
): ILocationContent {
|
||||
return {
|
||||
"body": text,
|
||||
"msgtype": MsgType.Location,
|
||||
"geo_uri": uri,
|
||||
[LOCATION_EVENT_TYPE.name]: {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: assetType ?? ASSET_TYPE_SELF,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: text,
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
// TODO: MSC1767 fallbacks m.image thumbnail
|
||||
};
|
||||
}
|
||||
|
||||
+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";
|
||||
@@ -33,6 +33,7 @@ import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -175,7 +176,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s) {
|
||||
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
@@ -304,7 +305,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;
|
||||
|
||||
|
||||
+21
-13
@@ -28,7 +28,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
|
||||
import { MatrixClient } from "../client";
|
||||
import { IDownloadKeyResult, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { CryptoStore } from "./store/base";
|
||||
|
||||
@@ -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;
|
||||
@@ -756,17 +756,21 @@ class DeviceListUpdateSerialiser {
|
||||
opts.token = this.syncToken;
|
||||
}
|
||||
|
||||
const factories = [];
|
||||
const factories: Array<() => Promise<IDownloadKeyResult>> = [];
|
||||
for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
|
||||
const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
|
||||
factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
|
||||
}
|
||||
|
||||
chunkPromises(factories, 3).then(async (responses: any[]) => {
|
||||
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
chunkPromises(factories, 3).then(async (responses: IDownloadKeyResult[]) => {
|
||||
const dk: IDownloadKeyResult["device_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys: IDownloadKeyResult["master_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks: IDownloadKeyResult["self_signing_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks: IDownloadKeyResult["user_signing_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
@@ -811,8 +815,12 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
private async processQueryResponseForUser(
|
||||
userId: string,
|
||||
dkResponse: object,
|
||||
crossSigningResponse: any, // TODO types
|
||||
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
|
||||
crossSigningResponse: {
|
||||
master: IDownloadKeyResult["master_keys"]["user_id"];
|
||||
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
},
|
||||
): Promise<void> {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
|
||||
@@ -869,7 +877,7 @@ async function updateStoredDeviceKeysForUser(
|
||||
olmDevice: OlmDevice,
|
||||
userId: string,
|
||||
userStore: Record<string, DeviceInfo>,
|
||||
userResult: object,
|
||||
userResult: IDownloadKeyResult["device_keys"]["user_id"],
|
||||
localUserId: string,
|
||||
localDeviceId: string,
|
||||
): Promise<boolean> {
|
||||
|
||||
@@ -14,20 +14,15 @@ 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";
|
||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
|
||||
@@ -238,7 +233,7 @@ export class EncryptionSetupOperation {
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this.keyBackupInfo.algorithm,
|
||||
auth_data: this.keyBackupInfo.auth_data,
|
||||
@@ -248,7 +243,7 @@ export class EncryptionSetupOperation {
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, Method.Post, "/room_keys/version",
|
||||
undefined, this.keyBackupInfo,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
|
||||
+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
|
||||
*
|
||||
|
||||
+30
-20
@@ -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
|
||||
@@ -75,8 +83,8 @@ export class SecretStorage {
|
||||
private readonly baseApis?: MatrixClient,
|
||||
) {}
|
||||
|
||||
public async getDefaultKeyId(): Promise<string> {
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
public async getDefaultKeyId(): Promise<string | null> {
|
||||
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
|
||||
@@ -335,9 +345,9 @@ export class SecretStorage {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
// 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,10 +1112,10 @@ 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)=>{
|
||||
Object.keys(devicesInRoom).forEach((userId) => {
|
||||
Object.keys(devicesInRoom[userId]).forEach((deviceId) => {
|
||||
const device = devicesInRoom[userId][deviceId];
|
||||
if (device.isUnverified() && !device.isKnown()) {
|
||||
if (!unknownDevices[userId]) {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+8
-10
@@ -33,6 +33,7 @@ import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMegolmSessionData } from "./index";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
@@ -87,7 +88,7 @@ interface BackupAlgorithmClass {
|
||||
interface BackupAlgorithm {
|
||||
untrusted: boolean;
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||
free(): void;
|
||||
@@ -185,7 +186,6 @@ export class BackupManager {
|
||||
public async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
// eslint-disable-next-line camelcase
|
||||
): Promise<IPreparedKeyBackupVersion> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
@@ -300,7 +300,7 @@ export class BackupManager {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
sigs: [] as SigInfo[],
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -320,7 +320,7 @@ export class BackupManager {
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
@@ -645,9 +645,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
public async decryptSessions(
|
||||
sessions: Record<string, IKeyBackupSession>,
|
||||
): Promise<Record<string, any>[]> {
|
||||
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
@@ -658,7 +656,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
@@ -777,8 +775,8 @@ export class Aes256 implements BackupAlgorithm {
|
||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
||||
}
|
||||
|
||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
|
||||
const keys = [];
|
||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
|
||||
+11
-10
@@ -14,16 +14,16 @@ 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";
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
import { Method } from "../http-api";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -42,13 +42,13 @@ export interface IDeviceKeys {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: Signatures;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
@@ -206,9 +206,10 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
|
||||
// eslint-disable-next-line camelcase
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
|
||||
undefined,
|
||||
"PUT",
|
||||
Method.Put,
|
||||
"/dehydrated_device",
|
||||
undefined,
|
||||
{
|
||||
@@ -243,7 +244,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
const oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: IOneTimeKey = { key };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
@@ -271,7 +272,7 @@ export class DehydrationManager {
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto.baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
Method.Post,
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
undefined,
|
||||
{
|
||||
|
||||
+113
-84
@@ -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,13 +172,20 @@ interface ISignableObject {
|
||||
}
|
||||
|
||||
export interface IEventDecryptionResult {
|
||||
clearEvent: object;
|
||||
clearEvent: IClearEvent;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
untrusted?: boolean;
|
||||
}
|
||||
|
||||
export interface IRequestsMap {
|
||||
getRequest(event: MatrixEvent): VerificationRequest;
|
||||
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
|
||||
setRequest(event: MatrixEvent, request: VerificationRequest): void;
|
||||
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
||||
}
|
||||
|
||||
export class Crypto extends EventEmitter {
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
@@ -259,6 +255,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private oneTimeKeyCount: number;
|
||||
private needsNewFallback: boolean;
|
||||
private fallbackCleanup?: number; // setTimeout ID
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
@@ -762,12 +759,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 +780,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 +796,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 } }, "",
|
||||
@@ -819,7 +815,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => {
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
|
||||
if (
|
||||
this.crossSigningInfo.getId() &&
|
||||
await this.crossSigningInfo.isStoredInKeyCache("master")
|
||||
@@ -880,7 +876,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 +894,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
|
||||
@@ -1030,7 +1024,7 @@ export class Crypto extends EventEmitter {
|
||||
public isSecretStored(
|
||||
name: string,
|
||||
checkKey?: boolean,
|
||||
): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
return this.secretStorage.isStored(name, checkKey);
|
||||
}
|
||||
|
||||
@@ -1041,7 +1035,7 @@ export class Crypto extends EventEmitter {
|
||||
return this.secretStorage.request(name, devices);
|
||||
}
|
||||
|
||||
public getDefaultSecretStorageKeyId(): Promise<string> {
|
||||
public getDefaultSecretStorageKeyId(): Promise<string | null> {
|
||||
return this.secretStorage.getDefaultKeyId();
|
||||
}
|
||||
|
||||
@@ -1161,7 +1155,7 @@ export class Crypto extends EventEmitter {
|
||||
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
|
||||
logger.info(`Starting background key sig upload for ${this.deviceId}`);
|
||||
|
||||
const upload = ({ shouldEmit }) => {
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
return this.baseApis.uploadKeySignatures({
|
||||
[this.userId]: {
|
||||
[this.deviceId]: signedDevice,
|
||||
@@ -1197,7 +1191,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// Check all users for signatures if upgrade callback present
|
||||
// FIXME: do this in batches
|
||||
const users = {};
|
||||
const users: Record<string, IDeviceVerificationUpgrade> = {};
|
||||
for (const [userId, crossSigningInfo]
|
||||
of Object.entries(this.deviceList.crossSigningInfo)) {
|
||||
const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
|
||||
@@ -1274,7 +1268,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[] = [];
|
||||
@@ -1495,7 +1489,7 @@ export class Crypto extends EventEmitter {
|
||||
!crossSigningPrivateKeys.has("user_signing")
|
||||
);
|
||||
|
||||
const keySignatures = {};
|
||||
const keySignatures: Record<string, ISignedKey> = {};
|
||||
|
||||
if (selfSigningChanged) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
@@ -1550,7 +1544,7 @@ export class Crypto extends EventEmitter {
|
||||
// We may have existing signatures from deleted devices, which will cause
|
||||
// the entire upload to fail.
|
||||
keySignatures[this.crossSigningInfo.getId()] = Object.assign(
|
||||
{},
|
||||
{} as ISignedKey,
|
||||
masterKey,
|
||||
{
|
||||
signatures: {
|
||||
@@ -1564,7 +1558,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
const keysToUpload = Object.keys(keySignatures);
|
||||
if (keysToUpload.length) {
|
||||
const upload = ({ shouldEmit }) => {
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
logger.info(`Starting background key sig upload for ${keysToUpload}`);
|
||||
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
|
||||
.then((response) => {
|
||||
@@ -1609,7 +1603,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 +1858,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 +1921,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 };
|
||||
@@ -1924,7 +1934,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
|
||||
const oneTimeJson = {};
|
||||
const oneTimeJson: Record<string, { key: string }> = {};
|
||||
|
||||
for (const keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
@@ -1938,10 +1948,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;
|
||||
@@ -2060,7 +2083,7 @@ export class Crypto extends EventEmitter {
|
||||
);
|
||||
const device = await this.crossSigningInfo.signUser(xsk);
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit }) => {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
logger.info("Uploading signature for " + userId + "...");
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2133,7 +2156,7 @@ export class Crypto extends EventEmitter {
|
||||
logger.info("Own device " + deviceId + " marked verified: signing");
|
||||
|
||||
// Signing only needed if other device not already signed
|
||||
let device;
|
||||
let device: ISignedKey;
|
||||
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
|
||||
if (deviceTrust.isCrossSigningVerified()) {
|
||||
logger.log(`Own device ${deviceId} already cross-signing verified`);
|
||||
@@ -2144,7 +2167,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit }) => {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2188,11 +2211,7 @@ export class Crypto extends EventEmitter {
|
||||
return Promise.resolve(existingRequest);
|
||||
}
|
||||
const channel = new InRoomChannel(this.baseApis, roomId, userId);
|
||||
return this.requestVerificationWithChannel(
|
||||
userId,
|
||||
channel,
|
||||
this.inRoomVerificationRequests,
|
||||
);
|
||||
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
|
||||
}
|
||||
|
||||
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
|
||||
@@ -2204,17 +2223,13 @@ export class Crypto extends EventEmitter {
|
||||
return Promise.resolve(existingRequest);
|
||||
}
|
||||
const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
|
||||
return this.requestVerificationWithChannel(
|
||||
userId,
|
||||
channel,
|
||||
this.toDeviceVerificationRequests,
|
||||
);
|
||||
return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
|
||||
}
|
||||
|
||||
private async requestVerificationWithChannel(
|
||||
userId: string,
|
||||
channel: IVerificationChannel,
|
||||
requestsMap: any, // TODO types
|
||||
requestsMap: IRequestsMap,
|
||||
): Promise<VerificationRequest> {
|
||||
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
|
||||
// if transaction id is already known, add request
|
||||
@@ -2592,13 +2607,17 @@ 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>>> {
|
||||
const devicesByUser = {};
|
||||
public ensureOlmSessionsForUsers(
|
||||
users: string[],
|
||||
force?: boolean,
|
||||
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
@@ -2622,7 +2641,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2631,7 +2650,7 @@ export class Crypto extends EventEmitter {
|
||||
* @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
|
||||
*/
|
||||
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||
const exportedSessions = [];
|
||||
const exportedSessions: IMegolmSessionData[] = [];
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
|
||||
@@ -2658,7 +2677,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 +2704,7 @@ export class Crypto extends EventEmitter {
|
||||
successes++;
|
||||
if (opts.progressCallback) { updateProgress(); }
|
||||
});
|
||||
}));
|
||||
})).then();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2709,7 +2728,6 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -2720,8 +2738,6 @@ export class Crypto extends EventEmitter {
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
// TODO this return type lies
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
@@ -2763,8 +2779,7 @@ export class Crypto extends EventEmitter {
|
||||
delete content['io.element.performance_metrics'];
|
||||
}
|
||||
|
||||
const encryptedContent = await alg.encryptMessage(
|
||||
room, event.getType(), content);
|
||||
const encryptedContent = await alg.encryptMessage(room, event.getType(), content);
|
||||
|
||||
if (mRelatesTo) {
|
||||
encryptedContent['m.relates_to'] = mRelatesTo;
|
||||
@@ -2801,7 +2816,7 @@ export class Crypto extends EventEmitter {
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
redacted_because: decryptedEvent.clearEvent,
|
||||
redacted_because: decryptedEvent.clearEvent as IEvent,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3134,7 +3149,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
|
||||
return;
|
||||
}
|
||||
const createRequest = event => {
|
||||
const createRequest = (event: MatrixEvent) => {
|
||||
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
|
||||
return;
|
||||
}
|
||||
@@ -3152,11 +3167,7 @@ export class Crypto extends EventEmitter {
|
||||
return new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
};
|
||||
this.handleVerificationEvent(
|
||||
event,
|
||||
this.toDeviceVerificationRequests,
|
||||
createRequest,
|
||||
);
|
||||
this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3179,7 +3190,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
|
||||
return;
|
||||
}
|
||||
const createRequest = event => {
|
||||
const createRequest = (event: MatrixEvent) => {
|
||||
const channel = new InRoomChannel(
|
||||
this.baseApis,
|
||||
event.getRoomId(),
|
||||
@@ -3187,20 +3198,38 @@ export class Crypto extends EventEmitter {
|
||||
return new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
};
|
||||
this.handleVerificationEvent(
|
||||
event,
|
||||
this.inRoomVerificationRequests,
|
||||
createRequest,
|
||||
liveEvent,
|
||||
);
|
||||
this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
|
||||
};
|
||||
|
||||
private async handleVerificationEvent(
|
||||
event: MatrixEvent,
|
||||
requestsMap: any, // TODO types
|
||||
createRequest: any, // TODO types
|
||||
requestsMap: IRequestsMap,
|
||||
createRequest: (event: MatrixEvent) => VerificationRequest,
|
||||
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) {
|
||||
@@ -3289,7 +3318,7 @@ export class Crypto extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devicesByUser = {};
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
devicesByUser[sender] = [device];
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+20
-17
@@ -21,15 +21,15 @@ 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";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
enum Algorithm {
|
||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -126,13 +126,18 @@ 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),
|
||||
);
|
||||
}
|
||||
|
||||
interface IExistingOlmSession {
|
||||
device: DeviceInfo;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing olm sessions for the given devices, and the devices that
|
||||
* don't have olm sessions.
|
||||
@@ -153,11 +158,11 @@ export async function getExistingOlmSessions(
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
) {
|
||||
const devicesWithoutSession = {};
|
||||
const sessions = {};
|
||||
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
|
||||
const devicesWithoutSession: {[userId: string]: DeviceInfo[]} = {};
|
||||
const sessions: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
|
||||
|
||||
const promises = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
@@ -231,10 +236,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
force = false;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
const devicesWithoutSession: [string, string][] = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const result: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
|
||||
const resolveSession: Record<string, (sessionId?: string) => void> = {};
|
||||
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
@@ -322,9 +327,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
@@ -338,8 +341,8 @@ export async function ensureOlmSessionsForDevices(
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otkResult = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
const otkResult = res.one_time_keys || {} as IClaimOTKsResult["one_time_keys"];
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
@@ -360,7 +363,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
const deviceRes = userRes[deviceId] || {};
|
||||
let oneTimeKey = null;
|
||||
let oneTimeKey: IOneTimeKey = null;
|
||||
for (const keyId in deviceRes) {
|
||||
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
@@ -442,7 +445,7 @@ async function _verifyKeyAndStartSession(
|
||||
|
||||
export interface IObject {
|
||||
unsigned?: object;
|
||||
signatures?: object;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -200,10 +200,10 @@ export class Backend implements CryptoStore {
|
||||
|
||||
// index into the wantedStates array
|
||||
let stateIndex = 0;
|
||||
let result;
|
||||
let result: OutgoingRoomKeyRequest;
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
// got a match
|
||||
result = cursor.value;
|
||||
@@ -218,7 +218,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
|
||||
@@ -255,10 +255,10 @@ export class Backend implements CryptoStore {
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
let stateIndex = 0;
|
||||
const results = [];
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (keyReq.recipients.includes({ userId, deviceId })) {
|
||||
@@ -274,7 +274,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
}
|
||||
@@ -306,10 +306,10 @@ export class Backend implements CryptoStore {
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
let result = null;
|
||||
let result: OutgoingRoomKeyRequest = null;
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +444,7 @@ export class Backend implements CryptoStore {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const idx = objectStore.index("deviceKey");
|
||||
const getReq = idx.openCursor(deviceKey);
|
||||
const results = {};
|
||||
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
@@ -734,7 +734,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const rooms = {};
|
||||
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
@@ -756,7 +756,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sessions = [];
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
const txn = this.db.transaction(
|
||||
["sessions_needing_backup", "inbound_group_sessions"],
|
||||
@@ -877,8 +877,8 @@ export class Backend implements CryptoStore {
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log: PrefixedLogger = logger,
|
||||
): Promise<T> {
|
||||
let startTime;
|
||||
let description;
|
||||
let startTime: number;
|
||||
let description: string;
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
const txnId = this.nextTxnId++;
|
||||
startTime = Date.now();
|
||||
|
||||
@@ -175,8 +175,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
|
||||
const ret = [];
|
||||
const notifiedErrorDevices = getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(
|
||||
this.store, KEY_NOTIFIED_ERROR_DEVICES,
|
||||
) || {};
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
@@ -291,7 +293,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const result = {};
|
||||
const result: Record<string, IRoomEncryption> = {};
|
||||
const prefix = keyEndToEndRoomsPrefix('');
|
||||
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
@@ -306,7 +308,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const sessions = [];
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
for (const session in sessionsNeedingBackup) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -30,4 +30,5 @@ export interface IVerificationChannel {
|
||||
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
|
||||
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
|
||||
canCreateRequest(type: string): boolean;
|
||||
handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { IVerificationChannel } from "./Channel";
|
||||
import { EventType } from "../../../@types/event";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { IRequestsMap } from "../..";
|
||||
|
||||
const MESSAGE_TYPE = EventType.RoomMessage;
|
||||
const M_REFERENCE = "m.reference";
|
||||
@@ -36,7 +37,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.
|
||||
@@ -304,7 +305,7 @@ export class InRoomChannel implements IVerificationChannel {
|
||||
}
|
||||
}
|
||||
|
||||
export class InRoomRequests {
|
||||
export class InRoomRequests implements IRequestsMap {
|
||||
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
|
||||
|
||||
public getRequest(event: MatrixEvent): VerificationRequest {
|
||||
@@ -328,7 +329,7 @@ export class InRoomRequests {
|
||||
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
|
||||
}
|
||||
|
||||
public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
|
||||
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
|
||||
this.doSetRequest(channel.roomId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ import { errorFromEvent, newUnexpectedMessageError } from "../Error";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { IRequestsMap } from '../..';
|
||||
|
||||
type Request = VerificationRequest<ToDeviceChannel>;
|
||||
export type Request = VerificationRequest<ToDeviceChannel>;
|
||||
|
||||
/**
|
||||
* A key verification channel that sends verification events over to_device messages.
|
||||
@@ -276,7 +277,7 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
|
||||
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
|
||||
if (devices.length) {
|
||||
const msgMap = {};
|
||||
const msgMap: Record<string, Record<string, any>> = {};
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = content;
|
||||
}
|
||||
@@ -294,7 +295,7 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
}
|
||||
}
|
||||
|
||||
export class ToDeviceRequests {
|
||||
export class ToDeviceRequests implements IRequestsMap {
|
||||
private requestsByUserId = new Map<string, Map<string, Request>>();
|
||||
|
||||
public getRequest(event: MatrixEvent): Request {
|
||||
|
||||
@@ -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,
|
||||
@@ -581,7 +582,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
// get common methods
|
||||
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
|
||||
if (!this.wasSentByOwnDevice(event)) {
|
||||
const content = event.getContent();
|
||||
const content = event.getContent<{
|
||||
methods: string[];
|
||||
}>();
|
||||
this.commonMethods =
|
||||
content.methods.filter(m => this.verificationMethods.has(m));
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
+51
-4
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelationType } from "./@types/event";
|
||||
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
/**
|
||||
@@ -46,6 +48,7 @@ export interface IFilterComponent {
|
||||
not_senders?: string[];
|
||||
contains_url?: boolean;
|
||||
limit?: number;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -61,7 +64,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent) {}
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -69,11 +72,24 @@ export class FilterComponent {
|
||||
* @return {boolean} true if the event matches the filter
|
||||
*/
|
||||
public check(event: MatrixEvent): boolean {
|
||||
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
|
||||
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
|
||||
// Relation senders allows in theory a look-up of any senders
|
||||
// however clients can only know about the current user participation status
|
||||
// as sending a whole list of participants could be proven problematic in terms
|
||||
// of performance
|
||||
// This should be improved when bundled relationships solve that problem
|
||||
const relationSenders = [];
|
||||
if (this.userId && relations?.[RelationType.Thread]?.current_user_participated) {
|
||||
relationSenders.push(this.userId);
|
||||
}
|
||||
return this.checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
event.getType(),
|
||||
event.getContent() ? event.getContent().url !== undefined : false,
|
||||
relations,
|
||||
relationSenders,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,6 +105,8 @@ export class FilterComponent {
|
||||
senders: this.filterJson.senders || null,
|
||||
not_senders: this.filterJson.not_senders || [],
|
||||
contains_url: this.filterJson.contains_url || null,
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,9 +116,18 @@ export class FilterComponent {
|
||||
* @param {String} sender the sender of the event being checked
|
||||
* @param {String} eventType the type of the event being checked
|
||||
* @param {boolean} containsUrl whether the event contains a content.url field
|
||||
* @param {boolean} relationTypes whether has aggregated relation of the given type
|
||||
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
|
||||
* @return {boolean} true if the event fields match the filter
|
||||
*/
|
||||
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
|
||||
private checkFields(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
eventType: string,
|
||||
containsUrl: boolean,
|
||||
relationTypes: Array<RelationType | string>,
|
||||
relationSenders: string[],
|
||||
): boolean {
|
||||
const literalKeys = {
|
||||
"rooms": function(v: string): boolean {
|
||||
return roomId === v;
|
||||
@@ -133,15 +160,35 @@ export class FilterComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name];
|
||||
if (relationTypesFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATION_SENDERS.name];
|
||||
if (relationSendersFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
|
||||
return values.length > 0 && values.every(value => {
|
||||
return filter.includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a list of events down to those which match this filter component
|
||||
* @param {MatrixEvent[]} events Events to be checked against the filter component
|
||||
* @return {MatrixEvent[]} events which matched the filter component
|
||||
*/
|
||||
filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
public filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
return events.filter(this.check, this);
|
||||
}
|
||||
|
||||
@@ -150,7 +197,7 @@ export class FilterComponent {
|
||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||
* @return {Number} the limit for this filter component.
|
||||
*/
|
||||
limit(): number {
|
||||
public limit(): number {
|
||||
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -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?: Array<EventType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
|
||||
}
|
||||
|
||||
interface IStateFilter extends IRoomEventFilter {}
|
||||
@@ -167,8 +185,8 @@ export class Filter {
|
||||
}
|
||||
}
|
||||
|
||||
this.roomFilter = new FilterComponent(roomFilterFields);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {});
|
||||
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
|
||||
|
||||
// don't bother porting this from synapse yet:
|
||||
// this._room_state_filter =
|
||||
|
||||
+329
-203
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -20,14 +20,21 @@ limitations under the License.
|
||||
* @module http-api
|
||||
*/
|
||||
|
||||
import { parse as parseContentType } from "content-type";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import type { IncomingHttpHeaders, IncomingMessage } from "http";
|
||||
import type { Request as _Request, CoreOptions } from "request";
|
||||
// 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.
|
||||
import * as callbacks from "./realtime-callbacks";
|
||||
import { IUploadOpts } from "./@types/requests";
|
||||
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
|
||||
import { IDeferred } from "./utils";
|
||||
import { Callback } from "./client";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
/*
|
||||
TODO:
|
||||
@@ -40,6 +47,11 @@ TODO:
|
||||
*/
|
||||
export const PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for release v1 of the Client-Server HTTP API.
|
||||
*/
|
||||
export const PREFIX_V1 = "/_matrix/client/v1";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
@@ -61,10 +73,95 @@ export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
|
||||
*/
|
||||
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
|
||||
type RequestProps = "method"
|
||||
| "withCredentials"
|
||||
| "json"
|
||||
| "headers"
|
||||
| "qs"
|
||||
| "body"
|
||||
| "qsStringifyOptions"
|
||||
| "useQuerystring"
|
||||
| "timeout";
|
||||
|
||||
export interface IHttpOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
prefix: string;
|
||||
onlyData: boolean;
|
||||
accessToken?: string;
|
||||
extraParams?: Record<string, string>;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
request(opts: Pick<CoreOptions, RequestProps> & {
|
||||
uri: string;
|
||||
method: Method;
|
||||
// eslint-disable-next-line camelcase
|
||||
_matrix_opts: IHttpOpts;
|
||||
}, callback: RequestCallback): IRequest;
|
||||
}
|
||||
|
||||
interface IRequest extends _Request {
|
||||
onprogress?(e: unknown): void;
|
||||
}
|
||||
|
||||
interface IRequestOpts<T> {
|
||||
prefix?: string;
|
||||
localTimeoutMs?: number;
|
||||
headers?: Record<string, string>;
|
||||
json?: boolean; // defaults to true
|
||||
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
|
||||
bodyParser?(body: string): T;
|
||||
}
|
||||
|
||||
export interface IUpload {
|
||||
loaded: number;
|
||||
total: number;
|
||||
promise: IAbortablePromise<unknown>;
|
||||
}
|
||||
|
||||
interface IContentUri {
|
||||
base: string;
|
||||
path: string;
|
||||
params: {
|
||||
// eslint-disable-next-line camelcase
|
||||
access_token: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ResponseType<T, O extends IRequestOpts<T> | void = void> =
|
||||
O extends { bodyParser: (body: string) => T } ? T :
|
||||
O extends { json: false } ? string :
|
||||
T;
|
||||
|
||||
interface IUploadResponse {
|
||||
// eslint-disable-next-line camelcase
|
||||
content_uri: string;
|
||||
}
|
||||
|
||||
// This type's defaults only work for the Browser
|
||||
// in the Browser we default rawResponse = false & onlyContentUri = true
|
||||
// in Node we default rawResponse = true & onlyContentUri = false
|
||||
export type UploadContentResponseType<O extends IUploadOpts> =
|
||||
O extends undefined ? string :
|
||||
O extends { rawResponse: true } ? string :
|
||||
O extends { onlyContentUri: true } ? string :
|
||||
O extends { rawResponse: false } ? IUploadResponse :
|
||||
O extends { onlyContentUri: false } ? IUploadResponse :
|
||||
string;
|
||||
|
||||
export enum Method {
|
||||
Get = "GET",
|
||||
Put = "PUT",
|
||||
Post = "POST",
|
||||
Delete = "DELETE",
|
||||
}
|
||||
|
||||
export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
|
||||
* @param {EventEmitter} eventEmitter The event emitter to use for emitting events
|
||||
* @param {Object} opts The options to use for this HTTP API.
|
||||
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
|
||||
* 'http://localhost:8008'.
|
||||
@@ -77,7 +174,7 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return an
|
||||
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token to send with requests. Can be
|
||||
* @param {string=} opts.accessToken The access_token to send with requests. Can be
|
||||
* null to not send an access token.
|
||||
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
@@ -86,39 +183,37 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
|
||||
* Authorization header instead of query param to send the access token to the server.
|
||||
*/
|
||||
export function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = opts.onlyData || false;
|
||||
this.event_emitter = event_emitter;
|
||||
this.opts = opts;
|
||||
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
|
||||
this.uploads = [];
|
||||
}
|
||||
export class MatrixHttpApi {
|
||||
private uploads: IUpload[] = [];
|
||||
|
||||
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
|
||||
}
|
||||
|
||||
MatrixHttpApi.prototype = {
|
||||
/**
|
||||
* Sets the baase URL for the identity server
|
||||
* Sets the base URL for the identity server
|
||||
* @param {string} url The new base url
|
||||
*/
|
||||
setIdBaseUrl: function(url) {
|
||||
public setIdBaseUrl(url: string): void {
|
||||
this.opts.idBaseUrl = url;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
* path and query parameters respectively.
|
||||
*/
|
||||
getContentUri: function() {
|
||||
const params = {
|
||||
access_token: this.opts.accessToken,
|
||||
};
|
||||
public getContentUri(): IContentUri {
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: "/_matrix/media/r0/upload",
|
||||
params: params,
|
||||
params: {
|
||||
access_token: this.opts.accessToken,
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content to the homeserver
|
||||
@@ -160,14 +255,17 @@ MatrixHttpApi.prototype = {
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
uploadContent: function(file, opts) {
|
||||
public uploadContent<O extends IUploadOpts>(
|
||||
file: FileType,
|
||||
opts?: O,
|
||||
): IAbortablePromise<UploadContentResponseType<O>> {
|
||||
if (utils.isFunction(opts)) {
|
||||
// opts used to be callback
|
||||
// opts used to be callback, backwards compatibility
|
||||
opts = {
|
||||
callback: opts,
|
||||
};
|
||||
} else if (opts === undefined) {
|
||||
opts = {};
|
||||
callback: opts as unknown as IUploadOpts["callback"],
|
||||
} as O;
|
||||
} else if (!opts) {
|
||||
opts = {} as O;
|
||||
}
|
||||
|
||||
// default opts.includeFilename to true (ignoring falsey values)
|
||||
@@ -175,8 +273,8 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
const contentType = opts.type || file.type || 'application/octet-stream';
|
||||
const fileName = opts.name || file.name;
|
||||
const contentType = opts.type || (file as File).type || 'application/octet-stream';
|
||||
const fileName = opts.name || (file as File).name;
|
||||
|
||||
// We used to recommend setting file.stream to the thing to upload on
|
||||
// Node.js. As of 2019-06-11, this is still in widespread use in various
|
||||
@@ -185,13 +283,14 @@ MatrixHttpApi.prototype = {
|
||||
// the browser now define a `stream` method, which leads to trouble
|
||||
// here, so we also check the type of `stream`.
|
||||
let body = file;
|
||||
if (body.stream && typeof body.stream !== "function") {
|
||||
const bodyStream = (body as File | Blob).stream; // this type is wrong but for legacy reasons is good enough
|
||||
if (bodyStream && typeof bodyStream !== "function") {
|
||||
logger.warn(
|
||||
"Using `file.stream` as the content to upload. Future " +
|
||||
"versions of the js-sdk will change this to expect `file` to " +
|
||||
"be the content directly.",
|
||||
);
|
||||
body = body.stream;
|
||||
body = bodyStream;
|
||||
}
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
@@ -234,8 +333,8 @@ MatrixHttpApi.prototype = {
|
||||
// (browser-request doesn't support progress either, which is also kind
|
||||
// of important here)
|
||||
|
||||
const upload = { loaded: 0, total: 0 };
|
||||
let promise;
|
||||
const upload = { loaded: 0, total: 0 } as IUpload;
|
||||
let promise: IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
|
||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||
@@ -243,7 +342,7 @@ MatrixHttpApi.prototype = {
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
let bodyParser = null;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
let body = JSON.parse(rawBody);
|
||||
if (onlyContentUri) {
|
||||
body = body.content_uri;
|
||||
@@ -256,25 +355,23 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
const defer = utils.defer();
|
||||
const defer = utils.defer<UploadContentResponseType<O>>();
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
upload.xhr = xhr;
|
||||
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
|
||||
|
||||
const timeout_fn = function() {
|
||||
const timeoutFn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get
|
||||
// a progress notification
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
let resp;
|
||||
let resp: string;
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
throw new AbortError();
|
||||
@@ -296,10 +393,10 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
};
|
||||
xhr.upload.addEventListener("progress", function(ev) {
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
if (opts.progressHandler) {
|
||||
opts.progressHandler({
|
||||
loaded: ev.loaded,
|
||||
@@ -315,9 +412,8 @@ MatrixHttpApi.prototype = {
|
||||
queryArgs.push("filename=" + encodeURIComponent(fileName));
|
||||
}
|
||||
|
||||
if (!this.useAuthorizationHeader) {
|
||||
queryArgs.push("access_token="
|
||||
+ encodeURIComponent(this.opts.accessToken));
|
||||
if (!this.opts.useAuthorizationHeader) {
|
||||
queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken));
|
||||
}
|
||||
|
||||
if (queryArgs.length > 0) {
|
||||
@@ -325,73 +421,69 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (this.useAuthorizationHeader) {
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
|
||||
}
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(body);
|
||||
promise = defer.promise;
|
||||
promise = defer.promise as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// dirty hack (as per _request) to allow the upload to be cancelled.
|
||||
// dirty hack (as per doRequest) to allow the upload to be cancelled.
|
||||
promise.abort = xhr.abort.bind(xhr);
|
||||
} else {
|
||||
const queryParams = {};
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers: { "Content-Type": contentType },
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
bodyParser,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
// remove the upload from the list on completion
|
||||
const promise0 = promise.finally(function() {
|
||||
for (let i = 0; i < self.uploads.length; ++i) {
|
||||
if (self.uploads[i] === upload) {
|
||||
self.uploads.splice(i, 1);
|
||||
upload.promise = promise.finally(() => {
|
||||
for (let i = 0; i < this.uploads.length; ++i) {
|
||||
if (this.uploads[i] === upload) {
|
||||
this.uploads.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}) as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// copy our dirty abort() method to the new promise
|
||||
promise0.abort = promise.abort;
|
||||
|
||||
upload.promise = promise0;
|
||||
upload.promise.abort = promise.abort;
|
||||
this.uploads.push(upload);
|
||||
|
||||
return promise0;
|
||||
},
|
||||
return upload.promise as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
}
|
||||
|
||||
cancelUpload: function(promise) {
|
||||
public cancelUpload(promise: IAbortablePromise<unknown>): boolean {
|
||||
if (promise.abort) {
|
||||
promise.abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
getCurrentUploads: function() {
|
||||
public getCurrentUploads(): IUpload[] {
|
||||
return this.uploads;
|
||||
},
|
||||
}
|
||||
|
||||
idServerRequest: function(
|
||||
callback,
|
||||
method,
|
||||
path,
|
||||
params,
|
||||
prefix,
|
||||
accessToken,
|
||||
) {
|
||||
public idServerRequest<T>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]>,
|
||||
prefix: string,
|
||||
accessToken: string,
|
||||
): Promise<T> {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
@@ -406,28 +498,27 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
const opts = {
|
||||
uri: fullUri,
|
||||
method: method,
|
||||
method,
|
||||
withCredentials: false,
|
||||
json: true, // we want a JSON response if we can
|
||||
_matrix_opts: this.opts,
|
||||
headers: {},
|
||||
};
|
||||
if (method === 'GET') {
|
||||
} as Parameters<IHttpOpts["request"]>[0];
|
||||
|
||||
if (method === Method.Get) {
|
||||
opts.qs = params;
|
||||
} else if (typeof params === "object") {
|
||||
opts.json = params;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
opts.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const defer = utils.defer();
|
||||
this.opts.request(
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData),
|
||||
);
|
||||
const defer = utils.defer<T>();
|
||||
this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData));
|
||||
return defer.promise;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver.
|
||||
@@ -448,7 +539,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -460,45 +551,45 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequest: function(callback, method, path, queryParams, data, opts) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (this.useAuthorizationHeader) {
|
||||
if (isFinite(opts)) {
|
||||
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: Record<string, string | string[]>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
if (!queryParams) queryParams = {};
|
||||
let requestOpts = (opts || {}) as O;
|
||||
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
if (isFinite(opts as number)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts,
|
||||
};
|
||||
requestOpts = {
|
||||
localTimeoutMs: opts as number,
|
||||
} as O;
|
||||
}
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
|
||||
if (!requestOpts.headers) {
|
||||
requestOpts.headers = {};
|
||||
}
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers.Authorization) {
|
||||
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
if (!requestOpts.headers.Authorization) {
|
||||
requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
}
|
||||
if (queryParams.access_token) {
|
||||
delete queryParams.access_token;
|
||||
}
|
||||
} else {
|
||||
if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
} else if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
|
||||
const requestPromise = this.request(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
);
|
||||
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
|
||||
|
||||
const self = this;
|
||||
requestPromise.catch(function(err) {
|
||||
requestPromise.catch((err: MatrixError) => {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
self.event_emitter.emit("Session.logged_out", err);
|
||||
this.eventEmitter.emit("Session.logged_out", err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
self.event_emitter.emit(
|
||||
this.eventEmitter.emit(
|
||||
"no_consent",
|
||||
err.message,
|
||||
err.data.consent_uri,
|
||||
@@ -509,7 +600,7 @@ MatrixHttpApi.prototype = {
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return requestPromise;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials.
|
||||
@@ -529,7 +620,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -541,15 +632,19 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
request: function(callback, method, path, queryParams, data, opts) {
|
||||
opts = opts || {};
|
||||
const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
|
||||
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O,
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
const prefix = opts?.prefix ?? this.opts.prefix;
|
||||
const fullUri = this.opts.baseUrl + prefix + path;
|
||||
|
||||
return this.requestOtherUrl(
|
||||
callback, method, fullUri, queryParams, data, opts,
|
||||
);
|
||||
},
|
||||
return this.requestOtherUrl<T, O>(callback, method, fullUri, queryParams, data, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to an arbitrary URL.
|
||||
@@ -568,7 +663,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -580,21 +675,24 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
opts) {
|
||||
if (opts === undefined || opts === null) {
|
||||
opts = {};
|
||||
} else if (isFinite(opts)) {
|
||||
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
let requestOpts = (opts || {}) as O;
|
||||
if (isFinite(opts as number)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts,
|
||||
};
|
||||
requestOpts = {
|
||||
localTimeoutMs: opts as number,
|
||||
} as O;
|
||||
}
|
||||
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, opts,
|
||||
);
|
||||
},
|
||||
return this.doRequest<T, O>(callback, method, uri, queryParams, data, requestOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path
|
||||
@@ -607,13 +705,13 @@ MatrixHttpApi.prototype = {
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @return {string} URL
|
||||
*/
|
||||
getUrl: function(path, queryParams, prefix) {
|
||||
public getUrl(path: string, queryParams: CoreOptions["qs"], prefix: string): string {
|
||||
let queryString = "";
|
||||
if (queryParams) {
|
||||
queryString = "?" + utils.encodeParams(queryParams);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -640,25 +738,32 @@ MatrixHttpApi.prototype = {
|
||||
* @return {Promise} a promise which resolves to either the
|
||||
* response object (if this.opts.onlyData is truthy), or the parsed
|
||||
* body. Rejects
|
||||
*
|
||||
* Generic T is the callback/promise resolve type
|
||||
* Generic O should be inferred
|
||||
*/
|
||||
_request: function(callback, method, uri, queryParams, data, opts) {
|
||||
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: Record<string, string>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O,
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback,
|
||||
);
|
||||
throw Error("Expected callback to be a function but got " + typeof callback);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
const self = this;
|
||||
if (this.opts.extraParams) {
|
||||
queryParams = {
|
||||
...queryParams,
|
||||
...this.opts.extraParams,
|
||||
...(queryParams || {}),
|
||||
...this.opts.extraParams,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = utils.extend({}, opts.headers || {});
|
||||
const json = opts.json === undefined ? true : opts.json;
|
||||
const headers = Object.assign({}, opts.headers || {});
|
||||
if (!opts) opts = {} as O;
|
||||
const json = opts.json ?? true;
|
||||
let bodyParser = opts.bodyParser;
|
||||
|
||||
// we handle the json encoding/decoding here, because request and
|
||||
@@ -677,17 +782,17 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
if (bodyParser === undefined) {
|
||||
bodyParser = function(rawBody) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
return JSON.parse(rawBody);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const defer = utils.defer();
|
||||
const defer = utils.defer<T>();
|
||||
|
||||
let timeoutId;
|
||||
let timeoutId: number;
|
||||
let timedOut = false;
|
||||
let req;
|
||||
let req: IRequest;
|
||||
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
|
||||
|
||||
const resetTimeout = () => {
|
||||
@@ -697,9 +802,7 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
timeoutId = callbacks.setTimeout(function() {
|
||||
timedOut = true;
|
||||
if (req && req.abort) {
|
||||
req.abort();
|
||||
}
|
||||
req?.abort?.();
|
||||
defer.reject(new MatrixError({
|
||||
error: "Locally timed out waiting for a response",
|
||||
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
|
||||
@@ -710,7 +813,7 @@ MatrixHttpApi.prototype = {
|
||||
};
|
||||
resetTimeout();
|
||||
|
||||
const reqPromise = defer.promise;
|
||||
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>;
|
||||
|
||||
try {
|
||||
req = this.opts.request(
|
||||
@@ -727,7 +830,7 @@ MatrixHttpApi.prototype = {
|
||||
headers: headers || {},
|
||||
_matrix_opts: this.opts,
|
||||
},
|
||||
function(err, response, body) {
|
||||
(err, response, body) => {
|
||||
if (localTimeoutMs) {
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
@@ -735,16 +838,13 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
const handlerFn = requestCallback(
|
||||
defer, callback, self.opts.onlyData,
|
||||
bodyParser,
|
||||
);
|
||||
const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser);
|
||||
handlerFn(err, response, body);
|
||||
},
|
||||
);
|
||||
if (req) {
|
||||
// This will only work in a browser, where opts.request is the
|
||||
// `browser-request` import. Currently `request` does not support progress
|
||||
// `browser-request` import. Currently, `request` does not support progress
|
||||
// updates - see https://github.com/request/request/pull/2346.
|
||||
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
|
||||
if ('onprogress' in req) {
|
||||
@@ -757,7 +857,9 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
// FIXME: This is EVIL, but I can't think of a better way to expose
|
||||
// abort() operations on underlying HTTP requests :(
|
||||
if (req.abort) reqPromise.abort = req.abort.bind(req);
|
||||
if (req.abort) {
|
||||
reqPromise.abort = req.abort.bind(req);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
defer.reject(ex);
|
||||
@@ -766,8 +868,21 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
}
|
||||
return reqPromise;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type RequestCallback = (err?: Error, response?: XMLHttpRequest | IncomingMessage, body?: string) => void;
|
||||
|
||||
// if using onlyData=false then wrap your expected data type in this generic
|
||||
export interface IResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
headers?: IncomingHttpHeaders;
|
||||
}
|
||||
|
||||
function getStatusCode(response: XMLHttpRequest | IncomingMessage): number {
|
||||
return (response as XMLHttpRequest).status || (response as IncomingMessage).statusCode;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a callback that can be invoked by an HTTP request on completion,
|
||||
@@ -783,17 +898,17 @@ MatrixHttpApi.prototype = {
|
||||
* response, otherwise the result object (with `code` and `data` fields)
|
||||
*
|
||||
*/
|
||||
const requestCallback = function(
|
||||
defer, userDefinedCallback, onlyData,
|
||||
bodyParser,
|
||||
) {
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
function requestCallback<T>(
|
||||
defer: IDeferred<T>,
|
||||
userDefinedCallback?: Callback<T>,
|
||||
onlyData = false,
|
||||
bodyParser?: (body: string) => T,
|
||||
): RequestCallback {
|
||||
return function(err: Error, response: XMLHttpRequest | IncomingMessage, body: string): void {
|
||||
if (err) {
|
||||
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
|
||||
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
|
||||
const aborted = err.name === "AbortError" || err === "aborted";
|
||||
const aborted = err.name === "AbortError" || (err as any as string) === "aborted";
|
||||
if (!aborted && !(err instanceof MatrixError)) {
|
||||
// browser-request just throws normal Error objects,
|
||||
// not `TypeError`s like fetch does. So just assume any
|
||||
@@ -801,13 +916,15 @@ const requestCallback = function(
|
||||
err = new ConnectionError("request failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
let data: T | string = body;
|
||||
|
||||
if (!err) {
|
||||
try {
|
||||
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
|
||||
if (httpStatus >= 400) {
|
||||
if (getStatusCode(response) >= 400) {
|
||||
err = parseErrorResponse(response, body);
|
||||
} else if (bodyParser) {
|
||||
body = bodyParser(body);
|
||||
data = bodyParser(body);
|
||||
}
|
||||
} catch (e) {
|
||||
err = new Error(`Error parsing server response: ${e}`);
|
||||
@@ -816,21 +933,26 @@ const requestCallback = function(
|
||||
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
userDefinedCallback(err);
|
||||
userDefinedCallback?.(err);
|
||||
} else if (onlyData) {
|
||||
defer.resolve(data as T);
|
||||
userDefinedCallback?.(null, data as T);
|
||||
} else {
|
||||
const res = {
|
||||
code: response.status || response.statusCode, // XMLHttpRequest vs http.IncomingMessage
|
||||
const res: IResponse<T> = {
|
||||
code: getStatusCode(response),
|
||||
|
||||
// XXX: why do we bother with this? it doesn't work for
|
||||
// XMLHttpRequest, so clearly we don't use it.
|
||||
headers: response.headers,
|
||||
data: body,
|
||||
headers: (response as IncomingMessage).headers,
|
||||
data: data as T,
|
||||
};
|
||||
defer.resolve(onlyData ? body : res);
|
||||
userDefinedCallback(null, onlyData ? body : res);
|
||||
// XXX: the variations in caller-expected types here are horrible,
|
||||
// typescript doesn't do conditional types based on runtime values
|
||||
defer.resolve(res as any as T);
|
||||
userDefinedCallback?.(null, res as any as T);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to turn an HTTP error response into a Javascript Error.
|
||||
@@ -842,8 +964,8 @@ const requestCallback = function(
|
||||
* @param {String} body raw body of the response
|
||||
* @returns {Error}
|
||||
*/
|
||||
function parseErrorResponse(response, body) {
|
||||
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
|
||||
function parseErrorResponse(response: XMLHttpRequest | IncomingMessage, body?: string) {
|
||||
const httpStatus = getStatusCode(response);
|
||||
const contentType = getResponseContentType(response);
|
||||
|
||||
let err;
|
||||
@@ -872,14 +994,14 @@ function parseErrorResponse(response, body) {
|
||||
* @param {XMLHttpRequest|http.IncomingMessage} response response object
|
||||
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
|
||||
*/
|
||||
function getResponseContentType(response) {
|
||||
function getResponseContentType(response: XMLHttpRequest | IncomingMessage): ParsedMediaType {
|
||||
let contentType;
|
||||
if (response.getResponseHeader) {
|
||||
if ((response as XMLHttpRequest).getResponseHeader) {
|
||||
// XMLHttpRequest provides getResponseHeader
|
||||
contentType = response.getResponseHeader("Content-Type");
|
||||
} else if (response.headers) {
|
||||
contentType = (response as XMLHttpRequest).getResponseHeader("Content-Type");
|
||||
} else if ((response as IncomingMessage).headers) {
|
||||
// request provides http.IncomingMessage which has a message.headers map
|
||||
contentType = response.headers['content-type'] || null;
|
||||
contentType = (response as IncomingMessage).headers['content-type'] || null;
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
@@ -893,6 +1015,12 @@ function getResponseContentType(response) {
|
||||
}
|
||||
}
|
||||
|
||||
interface IErrorJson extends Partial<IUsageLimit> {
|
||||
[key: string]: any; // extensible
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix error. This is a JavaScript Error with additional
|
||||
* information specific to the standard Matrix error response.
|
||||
@@ -905,8 +1033,11 @@ function getResponseContentType(response) {
|
||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
export class MatrixError extends Error {
|
||||
constructor(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
public readonly errcode: string;
|
||||
public readonly data: IErrorJson;
|
||||
public httpStatus?: number; // set by http-api
|
||||
|
||||
constructor(errorJson: IErrorJson = {}) {
|
||||
super(`MatrixError: ${errorJson.errcode}`);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
@@ -923,18 +1054,13 @@ export class MatrixError extends Error {
|
||||
* @constructor
|
||||
*/
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message, cause = undefined) {
|
||||
constructor(message: string, private readonly cause: Error = undefined) {
|
||||
super(message + (cause ? `: ${cause.message}` : ""));
|
||||
this._cause = cause;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "ConnectionError";
|
||||
}
|
||||
|
||||
get cause() {
|
||||
return this._cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
@@ -954,7 +1080,7 @@ export class AbortError extends Error {
|
||||
* @return {any} the result of the network operation
|
||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||
*/
|
||||
export async function retryNetworkOperation(maxAttempts, callback) {
|
||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
|
||||
let attempts = 0;
|
||||
let lastConnectionError = null;
|
||||
while (attempts < maxAttempts) {
|
||||
+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,11 +18,9 @@ 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";
|
||||
import { MatrixError } from "./http-api";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -35,6 +33,7 @@ export interface IInputs {
|
||||
emailAddress?: string;
|
||||
phoneCountry?: string;
|
||||
phoneNumber?: string;
|
||||
registrationToken?: string;
|
||||
}
|
||||
|
||||
export interface IStageStatus {
|
||||
@@ -49,7 +48,7 @@ export interface IAuthData {
|
||||
flows?: IFlow[];
|
||||
params?: Record<string, Record<string, any>>;
|
||||
errcode?: string;
|
||||
error?: MatrixError;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
@@ -61,12 +60,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 +79,7 @@ export interface IAuthDict {
|
||||
// eslint-disable-next-line camelcase
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
registrationToken?: string;
|
||||
}
|
||||
|
||||
class NoAuthFlowFoundError extends Error {
|
||||
@@ -357,12 +358,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 +450,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,8 @@ import { MatrixClient } from "../client";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { FileType } from "../http-api";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
@@ -37,7 +39,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 +126,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 };
|
||||
}
|
||||
|
||||
@@ -131,7 +141,14 @@ export class MSC3089Branch {
|
||||
const room = this.client.getRoom(this.roomId);
|
||||
if (!room) throw new Error("Unknown room");
|
||||
|
||||
const event = room.getUnfilteredTimelineSet().findEventById(this.id);
|
||||
let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
|
||||
|
||||
// keep scrolling back if needed until we find the event or reach the start of the room:
|
||||
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS).paginationToken) {
|
||||
await this.client.scrollback(room, 100);
|
||||
event = room.getUnfilteredTimelineSet().findEventById(this.id);
|
||||
}
|
||||
|
||||
if (!event) throw new Error("Failed to find event");
|
||||
|
||||
// Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
|
||||
@@ -142,16 +159,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: FileType,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<void> {
|
||||
@@ -198,7 +215,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 { FileType } from "../http-api";
|
||||
|
||||
/**
|
||||
* 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().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: FileType,
|
||||
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 {
|
||||
@@ -51,9 +53,15 @@ export enum DuplicateStrategy {
|
||||
Replace = "replace",
|
||||
}
|
||||
|
||||
export interface IRoomTimelineData {
|
||||
timeline: EventTimeline;
|
||||
liveEvent?: boolean;
|
||||
}
|
||||
|
||||
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 +110,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 +159,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.
|
||||
*
|
||||
@@ -205,7 +214,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||
*/
|
||||
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken?: string): void {
|
||||
public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void {
|
||||
// Each EventTimeline has RoomState objects tracking the state at the start
|
||||
// and end of that timeline. The copies at the end of the live timeline are
|
||||
// special because they will have listeners attached to monitor changes to
|
||||
@@ -590,7 +599,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
this.setRelationsTarget(event);
|
||||
this.aggregateRelations(event);
|
||||
|
||||
const data = {
|
||||
const data: IRoomTimelineData = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||
};
|
||||
|
||||
@@ -34,13 +34,13 @@ export class EventTimeline {
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the start of the timeline, or backwards in time.
|
||||
*/
|
||||
static BACKWARDS = Direction.Backward;
|
||||
public static readonly BACKWARDS = Direction.Backward;
|
||||
|
||||
/**
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the end of the timeline, or forwards in time.
|
||||
*/
|
||||
static FORWARDS = Direction.Forward;
|
||||
public static readonly FORWARDS = Direction.Forward;
|
||||
|
||||
/**
|
||||
* Static helper method to set sender and target properties
|
||||
|
||||
+283
-67
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2022 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.
|
||||
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
@@ -28,14 +29,15 @@ import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
} 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.
|
||||
@@ -89,6 +91,13 @@ export interface IUnsigned {
|
||||
redacted_because?: IEvent;
|
||||
transaction_id?: string;
|
||||
invite_room_state?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
latest_event: IEvent;
|
||||
count: number;
|
||||
current_user_participated?: boolean;
|
||||
}
|
||||
|
||||
export interface IEvent {
|
||||
@@ -110,7 +119,7 @@ export interface IEvent {
|
||||
age?: number;
|
||||
}
|
||||
|
||||
interface IAggregatedRelation {
|
||||
export interface IAggregatedRelation {
|
||||
origin_server_ts: number;
|
||||
event_id?: string;
|
||||
sender?: string;
|
||||
@@ -119,31 +128,52 @@ interface IAggregatedRelation {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface IEventRelation {
|
||||
export interface IEventRelation {
|
||||
rel_type: RelationType | string;
|
||||
event_id: string;
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string;
|
||||
"m.render_in"?: string[];
|
||||
};
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface IDecryptionResult {
|
||||
clearEvent: {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
content: IContent;
|
||||
unsigned?: IUnsigned;
|
||||
};
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
export interface IVisibilityEventRelation extends IEventRelation {
|
||||
visibility: "visible" | "hidden";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When an event is a visibility change event, as per MSC3531,
|
||||
* the visibility change implied by the event.
|
||||
*/
|
||||
export interface IVisibilityChange {
|
||||
/**
|
||||
* If `true`, the target event should be made visible.
|
||||
* Otherwise, it should be hidden.
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* The event id affected.
|
||||
*/
|
||||
eventId: string;
|
||||
|
||||
/**
|
||||
* Optionally, a human-readable reason explaining why
|
||||
* the event was hidden. Ignored if the event was made
|
||||
* visible.
|
||||
*/
|
||||
reason: string | null;
|
||||
}
|
||||
/* 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;
|
||||
@@ -155,6 +185,30 @@ export interface IDecryptOptions {
|
||||
isRetry?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*/
|
||||
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be displayed.
|
||||
*/
|
||||
export interface IMessageVisibilityVisible {
|
||||
readonly visible: true;
|
||||
}
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be hidden.
|
||||
*/
|
||||
export interface IMessageVisibilityHidden {
|
||||
readonly visible: false;
|
||||
/**
|
||||
* Optionally, a human-readable reason to show to the user indicating why the
|
||||
* message has been hidden (e.g. "Message Pending Moderation").
|
||||
*/
|
||||
readonly reason: string | null;
|
||||
}
|
||||
// A singleton implementing `IMessageVisibilityVisible`.
|
||||
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
|
||||
|
||||
export class MatrixEvent extends EventEmitter {
|
||||
private pushActions: IActionsObject = null;
|
||||
private _replacingEvent: MatrixEvent = null;
|
||||
@@ -162,6 +216,18 @@ export class MatrixEvent extends EventEmitter {
|
||||
private _isCancelled = false;
|
||||
private clearEvent?: IClearEvent;
|
||||
|
||||
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
|
||||
Note: We're returning this object, so any value stored here MUST be frozen.
|
||||
*/
|
||||
private visibility: MessageVisibility = MESSAGE_VISIBLE;
|
||||
|
||||
// Not all events will be extensible-event compatible, so cache a flag in
|
||||
// addition to a falsy cached event value. We check the flag later on in
|
||||
// a public getter to decide if the cache is valid.
|
||||
private _hasCachedExtEv = false;
|
||||
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
|
||||
|
||||
/* curve25519 key which we believe belongs to the sender of the event. See
|
||||
* getSenderKey()
|
||||
*/
|
||||
@@ -203,6 +269,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread: Thread = null;
|
||||
private threadId: string;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
@@ -210,20 +277,20 @@ export class MatrixEvent extends EventEmitter {
|
||||
* it to us and the time we're now constructing this event, but that's better
|
||||
* than assuming the local clock is in sync with the origin HS's clock.
|
||||
*/
|
||||
private readonly localTimestamp: number;
|
||||
public localTimestamp: number;
|
||||
|
||||
// XXX: these should be read-only
|
||||
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;
|
||||
|
||||
@@ -275,10 +342,29 @@ export class MatrixEvent extends EventEmitter {
|
||||
});
|
||||
|
||||
this.txnId = event.txn_id || null;
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unstable getter to try and get an extensible event. Note that this might
|
||||
* return a falsy value if the event could not be parsed as an extensible
|
||||
* event.
|
||||
*
|
||||
* @deprecated Use stable functions where possible.
|
||||
*/
|
||||
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
|
||||
if (!this._hasCachedExtEv) {
|
||||
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
|
||||
}
|
||||
return this._cachedExtEv;
|
||||
}
|
||||
|
||||
private invalidateExtensibleEvent() {
|
||||
// just reset the flag - that'll trick the getter into parsing a new event
|
||||
this._hasCachedExtEv = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event as though it would appear unencrypted. If the event is already not
|
||||
* encrypted, it is simply returned as-is.
|
||||
@@ -351,10 +437,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* Get the room_id for this event. This will return <code>undefined</code>
|
||||
* for <code>m.presence</code> events.
|
||||
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* @return {string?} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* </code>
|
||||
*/
|
||||
public getRoomId(): string {
|
||||
public getRoomId(): string | undefined {
|
||||
return this.event.room_id;
|
||||
}
|
||||
|
||||
@@ -419,38 +505,56 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the event ID of the replied event
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get replyEventId(): string {
|
||||
const relations = this.getWireContent()["m.relates_to"];
|
||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||
public get threadRootId(): string | undefined {
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === RelationType.Thread) {
|
||||
return relatesTo.event_id;
|
||||
} else {
|
||||
return this.threadId
|
||||
|| this.getThread()?.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
|
||||
*/
|
||||
public get isThreadRelation(): boolean {
|
||||
return !!this.threadRootId && this.threadId !== this.getId();
|
||||
}
|
||||
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
const replyTo = relatesTo?.["m.in_reply_to"];
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
const threadDetails = this
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
return relatesTo?.[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]
|
||||
|| (this.replyEventId && replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|
||||
|| this.thread instanceof Thread;
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
// hence us having to check both bundled relation and inspect the thread
|
||||
// model
|
||||
return !!threadDetails || (this.getThread()?.id === this.getId());
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId
|
||||
|| this.getWireContent()["m.relates_to"]?.event_id;
|
||||
return this.replyEventId || this.relationEventId;
|
||||
}
|
||||
|
||||
public get replyEventId(): string {
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to'];
|
||||
return mRelatesTo?.['m.in_reply_to']?.event_id;
|
||||
}
|
||||
|
||||
public get relationEventId(): string {
|
||||
return this.getWireContent()
|
||||
?.["m.relates_to"]
|
||||
?.event_id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,9 +584,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
* Get the age of this event. This represents the age of the event when the
|
||||
* event arrived at the device, and not the age of the event when this
|
||||
* function was called.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
* Can only be returned once the server has echo'ed back
|
||||
* @return {Number|undefined} The age of this event in milliseconds.
|
||||
*/
|
||||
public getAge(): number {
|
||||
public getAge(): number | undefined {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
}
|
||||
|
||||
@@ -575,7 +680,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption() {
|
||||
return this.isEncrypted() && !this.isBeingDecrypted() && !this.clearEvent;
|
||||
if (this.isRedacted()) return false;
|
||||
if (this.isBeingDecrypted()) return false;
|
||||
if (this.clearEvent) return false;
|
||||
if (!this.isEncrypted()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -684,8 +794,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,10 +883,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private badEncryptedMessage(reason: string): IDecryptionResult {
|
||||
private badEncryptedMessage(reason: string): IEventDecryptionResult {
|
||||
return {
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: " + reason + " **",
|
||||
@@ -797,7 +907,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;
|
||||
@@ -806,6 +916,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -823,7 +934,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
public isEncrypted(): boolean {
|
||||
return !this.isState() && this.event.type === "m.room.encrypted";
|
||||
return !this.isState() && this.event.type === EventType.RoomMessageEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -909,6 +1020,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.event.unsigned || {};
|
||||
}
|
||||
|
||||
public setUnsigned(unsigned: IUnsigned): void {
|
||||
this.event.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public unmarkLocallyRedacted(): boolean {
|
||||
const value = this._localRedactionEvent;
|
||||
this._localRedactionEvent = null;
|
||||
@@ -928,6 +1043,53 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
|
||||
* caused a change in the actual visibility of this event, either by making it
|
||||
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
||||
* changing the reason (if it was hidden).
|
||||
* @param visibilityEvent event holding a hide/unhide payload, or nothing
|
||||
* if the event is being reset to its original visibility (presumably
|
||||
* by a visibility event being redacted).
|
||||
*/
|
||||
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
|
||||
const visible = visibilityChange ? visibilityChange.visible : true;
|
||||
const reason = visibilityChange ? visibilityChange.reason : null;
|
||||
let change = false;
|
||||
if (this.visibility.visible !== visibilityChange.visible) {
|
||||
change = true;
|
||||
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
if (visible) {
|
||||
this.visibility = MESSAGE_VISIBLE;
|
||||
} else {
|
||||
this.visibility = Object.freeze({
|
||||
visible: false,
|
||||
reason: reason,
|
||||
});
|
||||
}
|
||||
if (change) {
|
||||
this.emit("Event.visibilityChange", this, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return instructions to display or hide the message.
|
||||
*
|
||||
* @returns Instructions determining whether the message
|
||||
* should be displayed.
|
||||
*/
|
||||
public messageVisibility(): MessageVisibility {
|
||||
// Note: We may return `this.visibility` without fear, as
|
||||
// this is a shallow frozen object.
|
||||
return this.visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
@@ -977,6 +1139,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -994,7 +1158,55 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {boolean} True if this event is a redaction
|
||||
*/
|
||||
public isRedaction(): boolean {
|
||||
return this.getType() === "m.room.redaction";
|
||||
return this.getType() === EventType.RoomRedaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the visibility change caused by this event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns If the event is a well-formed visibility change event,
|
||||
* an instance of `IVisibilityChange`, otherwise `null`.
|
||||
*/
|
||||
public asVisibilityChange(): IVisibilityChange | null {
|
||||
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
|
||||
// Not a visibility change event.
|
||||
return null;
|
||||
}
|
||||
const relation = this.getRelation();
|
||||
if (!relation || relation.rel_type != "m.reference") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const eventId = relation.event_id;
|
||||
if (!eventId) {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const content = this.getWireContent();
|
||||
const visible = !!content.visible;
|
||||
const reason = content.reason;
|
||||
if (reason && typeof reason != "string") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
// Well-formed visibility change event.
|
||||
return {
|
||||
visible,
|
||||
reason,
|
||||
eventId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event alters the visibility of another event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns {boolean} True if this event alters the visibility
|
||||
* of another event.
|
||||
*/
|
||||
public isVisibilityEvent(): boolean {
|
||||
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1058,6 +1270,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
// emit the event if it changed
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
}
|
||||
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1132,6 +1346,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit("Event.replaced", this);
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,11 +1366,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
|
||||
const relations = this.getUnsigned()["m.relations"];
|
||||
if (relations) {
|
||||
return relations[relType];
|
||||
}
|
||||
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
|
||||
return this.getUnsigned()["m.relations"]?.[relType];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1164,7 +1376,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {string?}
|
||||
*/
|
||||
public replacingEventId(): string | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
return replaceRelation.event_id;
|
||||
} else if (this._replacingEvent) {
|
||||
@@ -1189,7 +1401,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {Date?}
|
||||
*/
|
||||
public replacingEventDate(): Date | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
const ts = replaceRelation.origin_server_ts;
|
||||
if (Number.isFinite(ts)) {
|
||||
@@ -1355,9 +1567,13 @@ export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(): Thread {
|
||||
public getThread(): Thread | undefined {
|
||||
return this.thread;
|
||||
}
|
||||
|
||||
public setThreadId(threadId: string): void {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
}
|
||||
|
||||
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
|
||||
@@ -1376,15 +1592,15 @@ const REDACT_KEEP_KEYS = new Set([
|
||||
|
||||
// a map from event type to the .content keys we keep when an event is redacted
|
||||
const REDACT_KEEP_CONTENT_MAP = {
|
||||
'm.room.member': { 'membership': 1 },
|
||||
'm.room.create': { 'creator': 1 },
|
||||
'm.room.join_rules': { 'join_rule': 1 },
|
||||
'm.room.power_levels': {
|
||||
[EventType.RoomMember]: { 'membership': 1 },
|
||||
[EventType.RoomCreate]: { 'creator': 1 },
|
||||
[EventType.RoomJoinRules]: { 'join_rule': 1 },
|
||||
[EventType.RoomPowerLevels]: {
|
||||
'ban': 1, 'events': 1, 'events_default': 1,
|
||||
'kick': 1, 'redact': 1, 'state_default': 1,
|
||||
'users': 1, 'users_default': 1,
|
||||
},
|
||||
'm.room.aliases': { 'aliases': 1 },
|
||||
[EventType.RoomAliases]: { 'aliases': 1 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2022 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 { Relations } from "./relations";
|
||||
import { MatrixEvent } from "./event";
|
||||
|
||||
export class RelatedRelations {
|
||||
private relations: Relations[];
|
||||
|
||||
public constructor(relations: Relations[]) {
|
||||
this.relations = relations.filter(r => !!r);
|
||||
}
|
||||
|
||||
public getRelations(): MatrixEvent[] {
|
||||
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
|
||||
}
|
||||
|
||||
public on(ev: string, fn: (...params) => void) {
|
||||
this.relations.forEach(r => r.on(ev, fn));
|
||||
}
|
||||
|
||||
public off(ev: string, fn: (...params) => void) {
|
||||
this.relations.forEach(r => r.off(ev, fn));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { EventStatus, MatrixEvent } from './event';
|
||||
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
|
||||
import { Room } from './room';
|
||||
import { logger } from '../logger';
|
||||
import { RelationType } from "../@types/event";
|
||||
@@ -319,7 +319,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
// the all-knowning server tells us that the event at some point had
|
||||
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
||||
|
||||
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -618,14 +618,14 @@ export class RoomState extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
|
||||
+562
-259
File diff suppressed because it is too large
Load Diff
+222
-145
@@ -14,48 +14,103 @@ 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 { ReEmitter } from "../ReEmitter";
|
||||
import { RelationType } from "../@types/event";
|
||||
import { IRelationsRequestOpts } from "../@types/requests";
|
||||
import { MatrixEvent, IThreadBundledRelationship } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RoomState } from "./room-state";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
Ready = "Thread.ready",
|
||||
Update = "Thread.update"
|
||||
Update = "Thread.update",
|
||||
NewReply = "Thread.newReply",
|
||||
ViewThread = "Thred.viewThread",
|
||||
}
|
||||
|
||||
interface ISerialisedThread {
|
||||
id: string;
|
||||
tails: string[];
|
||||
interface IThreadOpts {
|
||||
initialEvents?: MatrixEvent[];
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends EventEmitter {
|
||||
/**
|
||||
* A reference to the event ID at the top of the thread
|
||||
*/
|
||||
private root: string;
|
||||
export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
private reEmitter: ReEmitter;
|
||||
|
||||
private lastEvent: MatrixEvent;
|
||||
private replyCount = 0;
|
||||
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
|
||||
public initialEventsFetched = false;
|
||||
|
||||
public readonly id: string;
|
||||
|
||||
constructor(
|
||||
events: MatrixEvent[] = [],
|
||||
public readonly room: Room,
|
||||
public readonly client: MatrixClient,
|
||||
public readonly rootEvent: MatrixEvent | undefined,
|
||||
opts: IThreadOpts,
|
||||
) {
|
||||
super();
|
||||
this.timelineSet = new EventTimelineSet(room, {
|
||||
|
||||
this.room = opts.room;
|
||||
this.client = opts.client;
|
||||
this.timelineSet = new EventTimelineSet(this.room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
pendingEvents: true,
|
||||
});
|
||||
events.forEach(event => this.addEvent(event));
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.reEmitter.reEmit(this.timelineSet, [
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
]);
|
||||
|
||||
// If we weren't able to find the root event, it's probably missing
|
||||
// and we define the thread ID from one of the thread relation
|
||||
if (!rootEvent) {
|
||||
this.id = opts?.initialEvents
|
||||
?.find(event => event.isThreadRelation)?.relationEventId;
|
||||
} else {
|
||||
this.id = rootEvent.getId();
|
||||
}
|
||||
this.initialiseThread(this.rootEvent);
|
||||
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event));
|
||||
|
||||
this.room.on("Room.localEchoUpdated", this.onEcho);
|
||||
this.room.on("Room.timeline", this.onEcho);
|
||||
}
|
||||
|
||||
public get hasServerSideSupport(): boolean {
|
||||
return this.client.cachedCapabilities
|
||||
?.capabilities?.[RelationType.Thread]?.enabled;
|
||||
}
|
||||
|
||||
onEcho = (event: MatrixEvent) => {
|
||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
};
|
||||
|
||||
public get roomState(): RoomState {
|
||||
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,83 +120,104 @@ export class Thread extends EventEmitter {
|
||||
* @param event The event to add
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
|
||||
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
|
||||
return;
|
||||
// Add all incoming events to the thread's timeline set when there's
|
||||
// no server support
|
||||
if (!this.hasServerSideSupport) {
|
||||
if (this.timelineSet.findEventById(event.getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
}
|
||||
|
||||
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.hasServerSideSupport && this.initialEventsFetched) {
|
||||
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
false,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
roomState,
|
||||
);
|
||||
|
||||
if (this.ready) {
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if (!this.hasServerSideSupport && isThreadReply) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
// There is a risk that the `localTimestamp` approximation will not be accurate
|
||||
// when threads are used over federation. That could results in the reply
|
||||
// count value drifting away from the value returned by the server
|
||||
if (!this.lastEvent || (isThreadReply && event.localTimestamp > this.replyToEvent.localTimestamp)) {
|
||||
this.lastEvent = event;
|
||||
if (this.lastEvent.getId() !== this.id) {
|
||||
// This counting only works when server side support is enabled
|
||||
// as we started the counting from the value returned in the
|
||||
// bundled relationship
|
||||
if (this.hasServerSideSupport) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.NewReply, this, 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,
|
||||
);
|
||||
}
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
this.addEvent(mxEvent, true);
|
||||
if (mxEvent.replyEventId) {
|
||||
await this.fetchReplyChain();
|
||||
} else {
|
||||
await this.decryptEvents();
|
||||
this.emit(ThreadEvent.Ready, this);
|
||||
}
|
||||
if (this.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
this._currentUserParticipated = bundledRelationship.current_user_participated;
|
||||
|
||||
const event = new MatrixEvent(bundledRelationship.latest_event);
|
||||
this.setEventMetadata(event);
|
||||
this.lastEvent = event;
|
||||
}
|
||||
|
||||
if (!bundledRelationship && rootEvent) {
|
||||
this.addEvent(rootEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptEvents(): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
|
||||
return this.client.decryptEventIfNeeded(event, {});
|
||||
}),
|
||||
);
|
||||
public async fetchInitialEvents(): Promise<boolean> {
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,96 +228,97 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread ID, which is the same as the root event ID
|
||||
*/
|
||||
public get id(): string {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread root event
|
||||
*/
|
||||
public get rootEvent(): MatrixEvent {
|
||||
return this.findEventById(this.root);
|
||||
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
|
||||
for (let i = this.events.length - 1; i >= 0; i--) {
|
||||
const event = this.events[i];
|
||||
if (matches(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get roomId(): string {
|
||||
return this.rootEvent.getRoomId();
|
||||
return this.room.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.replyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
const events = this.events;
|
||||
return events[events.length -1];
|
||||
return this.lastEvent;
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
return this.timelineSet.getLiveTimeline().getEvents();
|
||||
}
|
||||
|
||||
public merge(thread: Thread): void {
|
||||
thread.events.forEach(event => {
|
||||
this.addEvent(event);
|
||||
});
|
||||
this.events.forEach(event => event.setThread(this));
|
||||
return this.liveTimeline.getEvents();
|
||||
}
|
||||
|
||||
public has(eventId: string): boolean {
|
||||
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
|
||||
}
|
||||
|
||||
public toJson(): ISerialisedThread {
|
||||
return {
|
||||
id: this.id,
|
||||
tails: Array.from(this.tail),
|
||||
};
|
||||
public get hasCurrentUserParticipated(): boolean {
|
||||
return this._currentUserParticipated;
|
||||
}
|
||||
|
||||
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
public get liveTimeline(): EventTimeline {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
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 async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
}> {
|
||||
let {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
} = await this.client.relations(
|
||||
this.room.roomId,
|
||||
this.id,
|
||||
RelationType.Thread,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
// When there's no nextBatch returned with a `from` request we have reached
|
||||
// the end of the thread, and therefore want to return an empty one
|
||||
if (!opts.to && !nextBatch) {
|
||||
events = [originalEvent, ...events];
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
this.setEventMetadata(event);
|
||||
}
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
prependEvents,
|
||||
this.liveTimeline,
|
||||
prependEvents ? nextBatch : prevBatch,
|
||||
);
|
||||
|
||||
return {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export class RoomHierarchy {
|
||||
if (!this.backRefs.has(childRoomId)) {
|
||||
this.backRefs.set(childRoomId, []);
|
||||
}
|
||||
this.backRefs.get(childRoomId).push(ev.room_id);
|
||||
this.backRefs.get(childRoomId).push(room.room_id);
|
||||
|
||||
// fill viaMap
|
||||
if (Array.isArray(ev.content.via)) {
|
||||
|
||||
+2
-2
@@ -70,7 +70,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
}
|
||||
// we ship with browser-request which returns { cors: rejected } when trying
|
||||
// with no connection, so if we match that, give up since they have no conn.
|
||||
if (err.cors === "rejected") {
|
||||
if (err["cors"] === "rejected") {
|
||||
return -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));
|
||||
};
|
||||
@@ -231,14 +231,14 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return new Promise<IEvent[] | null>((resolve, reject) =>{
|
||||
return new Promise<IEvent[] | null>((resolve, reject) => {
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||
const store = tx.objectStore("oob_membership_events");
|
||||
const roomIndex = store.index("room");
|
||||
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();
|
||||
@@ -256,8 +256,8 @@ function removeByPrefix(store, prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
function debuglog(...args) {
|
||||
if (DEBUG) {
|
||||
logger.log(...arguments);
|
||||
logger.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface IOpts {
|
||||
export interface IMinimalEvent {
|
||||
content: IContent;
|
||||
type: EventType | string;
|
||||
unsigned?: IUnsigned;
|
||||
}
|
||||
|
||||
export interface IEphemeral {
|
||||
|
||||
+50
-67
@@ -34,23 +34,22 @@ import { PushProcessor } from "./pushprocessor";
|
||||
import { logger } from './logger';
|
||||
import { InvalidStoreError } from './errors';
|
||||
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
||||
import { SyncState } from "./sync.api";
|
||||
import {
|
||||
Category,
|
||||
IEphemeral,
|
||||
IInvitedRoom,
|
||||
IInviteState,
|
||||
IJoinedRoom,
|
||||
ILeftRoom,
|
||||
IStateEvent,
|
||||
IMinimalEvent,
|
||||
IRoomEvent,
|
||||
IStateEvent,
|
||||
IStrippedState,
|
||||
ISyncResponse,
|
||||
ITimeline,
|
||||
IEphemeral,
|
||||
IMinimalEvent,
|
||||
} from "./sync-accumulator";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { MatrixError, Method } from "./http-api";
|
||||
import { ISavedSync } from "./store";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IPushRules } from "./@types/PushRules";
|
||||
@@ -68,6 +67,15 @@ const BUFFER_PERIOD_MS = 80 * 1000;
|
||||
// keepAlive is successful but the server /sync fails.
|
||||
const FAILED_SYNC_ERROR_THRESHOLD = 3;
|
||||
|
||||
export enum SyncState {
|
||||
Error = "ERROR",
|
||||
Prepared = "PREPARED",
|
||||
Stopped = "STOPPED",
|
||||
Syncing = "SYNCING",
|
||||
Catchup = "CATCHUP",
|
||||
Reconnecting = "RECONNECTING",
|
||||
}
|
||||
|
||||
function getFilterName(userId: string, suffix?: string): string {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
@@ -87,13 +95,19 @@ interface ISyncOptions {
|
||||
}
|
||||
|
||||
export interface ISyncStateData {
|
||||
error?: Error;
|
||||
error?: MatrixError;
|
||||
oldSyncToken?: string;
|
||||
nextSyncToken?: string;
|
||||
catchingUp?: boolean;
|
||||
fromCache?: boolean;
|
||||
}
|
||||
|
||||
enum SetPresence {
|
||||
Offline = "offline",
|
||||
Online = "online",
|
||||
Unavailable = "unavailable",
|
||||
}
|
||||
|
||||
interface ISyncParams {
|
||||
filter?: string;
|
||||
timeout: number;
|
||||
@@ -101,7 +115,7 @@ interface ISyncParams {
|
||||
// eslint-disable-next-line camelcase
|
||||
full_state?: boolean;
|
||||
// eslint-disable-next-line camelcase
|
||||
set_presence?: "offline" | "online" | "unavailable";
|
||||
set_presence?: SetPresence;
|
||||
_cacheBuster?: string | number; // not part of the API itself
|
||||
}
|
||||
|
||||
@@ -187,6 +201,7 @@ export class SyncApi {
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
"Room.replaceEvent",
|
||||
"Room.visibilityChange",
|
||||
]);
|
||||
this.registerStateListeners(room);
|
||||
return room;
|
||||
@@ -260,16 +275,16 @@ export class SyncApi {
|
||||
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
|
||||
).then(function(filterId) {
|
||||
qps.filter = filterId;
|
||||
return client.http.authedRequest(
|
||||
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
|
||||
return client.http.authedRequest<any>( // TODO types
|
||||
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
|
||||
);
|
||||
}).then((data) => {
|
||||
let leaveRooms = [];
|
||||
if (data.rooms && data.rooms.leave) {
|
||||
if (data.rooms?.leave) {
|
||||
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
||||
}
|
||||
const rooms = [];
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
rooms.push(room);
|
||||
if (!leaveObj.isBrandNewRoom) {
|
||||
@@ -285,7 +300,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);
|
||||
|
||||
@@ -295,7 +310,7 @@ export class SyncApi {
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
@@ -307,39 +322,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.
|
||||
@@ -433,9 +415,10 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// FIXME: gut wrenching; hard-coded timeout values
|
||||
this.client.http.authedRequest(undefined, "GET", "/events", {
|
||||
// TODO types
|
||||
this.client.http.authedRequest<any>(undefined, Method.Get, "/events", {
|
||||
room_id: peekRoom.roomId,
|
||||
timeout: 30 * 1000,
|
||||
timeout: String(30 * 1000),
|
||||
from: token,
|
||||
}, undefined, 50 * 1000).then((res) => {
|
||||
if (this._peekRoom !== peekRoom) {
|
||||
@@ -503,7 +486,7 @@ export class SyncApi {
|
||||
return this.syncStateData;
|
||||
}
|
||||
|
||||
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: Error): Promise<void> {
|
||||
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: MatrixError): Promise<void> {
|
||||
// Wait for the saved sync to complete - we send the pushrules and filter requests
|
||||
// before the saved sync has finished so they can run in parallel, but only process
|
||||
// the results after the saved sync is done. Equivalently, we wait for it to finish
|
||||
@@ -538,6 +521,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;
|
||||
@@ -897,8 +881,8 @@ export class SyncApi {
|
||||
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||
const qps = this.getSyncParams(syncOptions, syncToken);
|
||||
return this.client.http.authedRequest(
|
||||
undefined, "GET", "/sync", qps, undefined,
|
||||
return this.client.http.authedRequest( // TODO types
|
||||
undefined, Method.Get, "/sync", qps as any, undefined,
|
||||
qps.timeout + BUFFER_PERIOD_MS,
|
||||
);
|
||||
}
|
||||
@@ -933,7 +917,7 @@ export class SyncApi {
|
||||
};
|
||||
|
||||
if (this.opts.disablePresence) {
|
||||
qps.set_presence = "offline";
|
||||
qps.set_presence = SetPresence.Offline;
|
||||
}
|
||||
|
||||
if (syncToken) {
|
||||
@@ -956,7 +940,7 @@ export class SyncApi {
|
||||
return qps;
|
||||
}
|
||||
|
||||
private onSyncError(err: Error, syncOptions: ISyncOptions): void {
|
||||
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
|
||||
if (!this.running) {
|
||||
debuglog("Sync no longer running: exiting");
|
||||
if (this.connectionReturnedDefer) {
|
||||
@@ -1320,10 +1304,10 @@ 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);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1382,16 +1366,16 @@ export class SyncApi {
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
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);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
@@ -1448,11 +1432,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"),
|
||||
@@ -1506,7 +1493,7 @@ export class SyncApi {
|
||||
|
||||
this.client.http.request(
|
||||
undefined, // callback
|
||||
"GET", "/_matrix/client/versions",
|
||||
Method.Get, "/_matrix/client/versions",
|
||||
undefined, // queryParams
|
||||
undefined, // data
|
||||
{
|
||||
@@ -1733,12 +1720,8 @@ export class SyncApi {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
threadedEvents
|
||||
.sort((a, b) => a.getTs() - b.getTs())
|
||||
.forEach(event => {
|
||||
room.addThreadedEvent(event);
|
||||
});
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
|
||||
return this.client.processThreadEvents(room, threadedEvents);
|
||||
}
|
||||
|
||||
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user