Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 095e3a8a88 | |||
| 92623a246a | |||
| f2a2e7f5dd | |||
| 15b6a9a257 | |||
| 8f5082cd2f | |||
| 50c877cb60 | |||
| e3e7fd5900 | |||
| 69823f9cae | |||
| e5dcdf109a | |||
| 6c649e2bfe | |||
| 9747e5e0d5 | |||
| 2bd60f4160 | |||
| e71362a6df | |||
| 673c09cd77 | |||
| 4bc8f968e0 | |||
| 3705426b6d | |||
| 5813aea27f | |||
| 5ba836a31b | |||
| 4f387d15b9 | |||
| 8ab0246ca2 | |||
| ed68bd4108 | |||
| 3d39e8ec26 | |||
| 53bdab7cc8 | |||
| fafa55ba1d | |||
| 3a818c1419 | |||
| 422853298c | |||
| f3a2bd6b40 | |||
| 6834137f50 | |||
| c4d7fef0cd | |||
| 556b5ff628 | |||
| 2daa1ec4ce | |||
| 348ea72b70 | |||
| 4e14d810c5 | |||
| f84905b003 | |||
| 9bb87145f6 | |||
| c7675570aa | |||
| be942fd66a | |||
| 8544aca362 | |||
| c3308e1de9 | |||
| 047d7125a1 | |||
| cf4d3117c1 | |||
| 88c2844e48 | |||
| 6ca4694fe6 | |||
| 062287e270 | |||
| 50e0d76f10 | |||
| df039d3d8c | |||
| 799606b73c | |||
| c048f5d357 | |||
| f712a8cecb | |||
| e1d8d6bf4e | |||
| 14ed8a110c | |||
| cb6700f21b | |||
| d4f35bf07a | |||
| b4eb29138b | |||
| 8ca3a071f9 | |||
| 5abd211436 | |||
| 99f428e091 | |||
| a734fdf9b0 | |||
| 6f91b2bc80 | |||
| 32d7272939 | |||
| c8d85452bf | |||
| 557f800919 | |||
| 2119e88c78 | |||
| 24e7a7e6bf | |||
| 4e5847f356 | |||
| 72d580edfd | |||
| e878b7683b | |||
| fac700bf4d | |||
| 894c24880d | |||
| d742249fd1 | |||
| 9c100fea48 | |||
| 7daab62850 | |||
| fed0ced89d | |||
| df9cfaed1d | |||
| edd614dd0c | |||
| 375b228bb2 | |||
| 7143ef8a32 | |||
| 1bd7de5a18 | |||
| a1f73eee86 | |||
| d9f8710758 | |||
| 3a9d5439a2 | |||
| 5d1be6e8be | |||
| 775e85a2e8 | |||
| dc8f403c44 | |||
| cf9fa991d1 | |||
| d26dc49755 | |||
| be04559a66 | |||
| 0722dafe58 | |||
| ab906faa17 | |||
| 9d63a12469 | |||
| f40d0d24c7 | |||
| 2cdea36abf | |||
| 8d4f4c8832 | |||
| 3b4dcbb01d | |||
| b52f3b1c57 | |||
| 2cdf5629e5 | |||
| 97b4f0e369 | |||
| a508c8d236 | |||
| 7911e6371a | |||
| bbace01e9a | |||
| 87469fea63 | |||
| 48a9db0315 | |||
| 33c9471112 | |||
| 04fdcbb672 | |||
| 4d2286c5be | |||
| 620e45e4dc | |||
| fb97e74344 | |||
| 3bbb7df72d | |||
| 58b08821bf | |||
| 19bdfd5b84 | |||
| b6c857e2a2 | |||
| 0991626e85 | |||
| 7b10d4fac0 | |||
| 980dacaa87 | |||
| 3790e8abb4 | |||
| 14ba851daf | |||
| 95d80772d7 | |||
| 341e936082 | |||
| e87399c4a1 | |||
| a7678ea1e0 | |||
| 0388bd3b48 | |||
| f27327164b | |||
| a0f85075c1 | |||
| e97d18a03b | |||
| f07cdb43b1 | |||
| da8c8f166d | |||
| 0830a30498 | |||
| 68a2762b75 | |||
| 666e471369 | |||
| d89f4a74f9 | |||
| 191535d01a | |||
| 241203f804 | |||
| 26cc7a0f64 | |||
| 324f9e58ea | |||
| e71519c638 | |||
| 9b6cee0cab | |||
| 2a5f6a7b3d | |||
| cae03817cb | |||
| 37736184a0 | |||
| 9431a52abe | |||
| 13c664ad34 | |||
| d7640d9e15 | |||
| 3c36be9839 | |||
| 9dce7bbb97 | |||
| fa44300abc | |||
| 2783d162b7 | |||
| 39566dbd4f | |||
| f8186add92 | |||
| c15f22e81d | |||
| 052ac8f95b | |||
| c37f8ba4c7 | |||
| 7e2458d5b5 | |||
| fe989177bb | |||
| 412645f025 | |||
| e527c46f34 | |||
| da471b57ad | |||
| a4ff97c8df | |||
| 411fc47f28 | |||
| b97e3113d1 | |||
| f107d63fab | |||
| a65ac5a82b | |||
| c7c1db00e8 | |||
| 824f93daa3 | |||
| 9fec2f8c58 | |||
| 0b46389176 | |||
| db03f16b8e | |||
| f83fcb1a18 | |||
| ba0f1f86b9 | |||
| 81d349d993 | |||
| d65d2b94b9 | |||
| acd52bc070 | |||
| a64339763f | |||
| 4f21b95bbf | |||
| 03e4dcf1ed | |||
| 3c56d1372d | |||
| 27ecdef09b | |||
| 121e8a51c1 | |||
| 88e3ada701 | |||
| 92d822d494 | |||
| 62c93f54cc | |||
| 500c2ed4f2 | |||
| 425362edfe | |||
| 9c554f9b3d | |||
| b7a5f81f95 | |||
| 0746a64c83 | |||
| 22cd475d22 | |||
| 6904f6b36f | |||
| e25a649e1a | |||
| 0eb8cbee4d | |||
| 96e8f65af7 | |||
| 8fb036ba2d | |||
| 0a10fa12ef | |||
| e30dad7913 | |||
| c2a0c02898 | |||
| 7c1e89f86a | |||
| 29f8a4cba2 | |||
| b884accc99 | |||
| d56540320e | |||
| 526fe7e9a4 | |||
| aa696c4c15 | |||
| 3216d7e5a7 | |||
| b68f649414 | |||
| 543f35d9a8 | |||
| 03d42a98cd | |||
| 4f48554cd2 | |||
| 85d9c0f403 | |||
| 66b4279dd8 | |||
| 1ee84fbeca | |||
| c0e15b206a | |||
| 290575ba65 | |||
| bd8690de57 | |||
| 0d09f87777 | |||
| 0f45b7e516 | |||
| 4c552cc350 | |||
| 6a87f54292 | |||
| bfb2c5aad0 | |||
| db898e7bcf | |||
| dbebbe3a19 | |||
| ccf296ea92 | |||
| d65b48204b | |||
| 977720cee4 | |||
| 3c7cdb1da8 | |||
| dd80e744ff | |||
| 0b12e37459 | |||
| 51e815f0cb | |||
| f3856d569d | |||
| e6d1909f0b | |||
| 408976a199 | |||
| 4da49d926b | |||
| 75750ed760 | |||
| 2f74779bf1 | |||
| 7b038393b1 | |||
| 768c0e7f77 | |||
| c6009b1056 | |||
| 2a280afa88 | |||
| be980f4bc9 | |||
| 78118e9442 | |||
| a2c10a7913 | |||
| 7c4f7c9dad | |||
| 62058e6d48 | |||
| a2f514b544 | |||
| 14b424ee94 | |||
| 696b3ef1ce | |||
| 4e2ee3b3a8 | |||
| 1f9fab9a0c | |||
| d91d1cea34 | |||
| 69ba32683c | |||
| fc78e63ad1 | |||
| db212a555d | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| e5a67500e4 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 18113900fe | |||
| 88b8b24629 | |||
| 58c60d85e7 | |||
| 21a357c433 | |||
| d49d6936a5 | |||
| c2b5b14d26 | |||
| f1a0c46a29 | |||
| e775bcac3c |
@@ -1,3 +1,94 @@
|
||||
Changes in [14.0.0](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0) (2021-10-11)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
Changes in [13.0.0](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0) (2021-09-27)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [13.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0-rc.1) (2021-09-21)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
|
||||
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
|
||||
|
||||
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
|
||||
===================================================================================================
|
||||
|
||||
## 🔒 SECURITY FIXES
|
||||
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
|
||||
for details.
|
||||
|
||||
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
|
||||
===================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
|
||||
|
||||
## ✨ Features
|
||||
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
|
||||
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
|
||||
|
||||
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
|
||||
===================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)).
|
||||
|
||||
Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16)
|
||||
===================================================================================================
|
||||
|
||||
|
||||
+8
-2
@@ -22,8 +22,14 @@ ask you to make any refinements needed or merge it and make them ourselves.
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Notes for the reviewer that might help them to understand why the change is
|
||||
necessary or how they might better review it.
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch.
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "12.3.0",
|
||||
"version": "14.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"allchange": "^1.0.0",
|
||||
"allchange": "^1.0.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
|
||||
+4
-1
@@ -191,7 +191,10 @@ git commit package.json $pkglock -m "$tag"
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
||||
if [ "$?" -eq 0 ]; then
|
||||
signing_id=$result
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
describe("session reuse and key reshares", () => {
|
||||
let megolmEncryption;
|
||||
let aliceDeviceInfo;
|
||||
let mockRoom;
|
||||
let olmDevice;
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
beforeEach(async () => {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
aliceDeviceInfo = {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
});
|
||||
|
||||
megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
});
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
it("re-shares keys to devices it's already sent to", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-share keys to devices whose keys have changed", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
|
||||
);
|
||||
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
|
||||
@@ -288,7 +288,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
@@ -369,7 +369,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
@@ -546,7 +546,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
bobVerifier.verify();
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
bobVerifier.endTimer();
|
||||
});
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
@@ -77,6 +77,6 @@ describe("verification request integration tests with crypto layer", function()
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
aliceVerifier.endTimer();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("self-verifications", () => {
|
||||
undefined, // startEvent
|
||||
request,
|
||||
);
|
||||
verification._resolve = () => undefined;
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ describe("MatrixClient", function() {
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
@@ -270,9 +271,33 @@ describe("MatrixClient", function() {
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees if not joined", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "leave", // "not join"
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees for unknown rooms", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return null; // imply unknown
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
@@ -307,6 +332,7 @@ describe("MatrixClient", function() {
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
|
||||
@@ -127,6 +127,45 @@ describe("MSC3089Branch", () => {
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be unlocked by default', async () => {
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(false);
|
||||
});
|
||||
|
||||
it('should use lock status from index event', async () => {
|
||||
indexEvent.getContent = () => ({ active: true, locked: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be able to change its locked status', async () => {
|
||||
const locked = true;
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: true,
|
||||
locked: locked,
|
||||
});
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
await branch.setLocked(locked);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to return event information', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
@@ -151,4 +190,21 @@ describe("MSC3089Branch", () => {
|
||||
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to return the event object', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const eventsArr = [
|
||||
{ getId: () => "$not-file", getContent: () => ({}) },
|
||||
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
|
||||
];
|
||||
client.getEventTimeline = () => Promise.resolve({
|
||||
getEvents: () => eventsArr,
|
||||
}) as any as Promise<EventTimeline>; // partial
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
|
||||
const res = await branch.getFileEvent();
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toBe(eventsArr[1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,40 +227,61 @@ describe("MSC3089TreeSpace", () => {
|
||||
[targetUser]: expectedPl,
|
||||
},
|
||||
});
|
||||
|
||||
// Store new power levels so the `getPermissions()` test passes
|
||||
makePowerLevels(content);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setPermissions(targetUser, role);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
|
||||
const finalPermissions = tree.getPermissions(targetUser);
|
||||
expect(finalPermissions).toEqual(role);
|
||||
}
|
||||
|
||||
it('should support setting Viewer permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Editor permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
}, TreePermissions.Editor, 1024);
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support setting Owner permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1024,
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Owner, 1024);
|
||||
}, TreePermissions.Owner, 1026);
|
||||
});
|
||||
|
||||
it('should support demoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 2222,
|
||||
},
|
||||
@@ -270,11 +291,15 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should support promoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 5,
|
||||
},
|
||||
}, TreePermissions.Editor, 1024);
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support defaults: Viewer', () => {
|
||||
|
||||
@@ -90,8 +90,9 @@ export interface ISenderNotificationPermissionCondition
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type PushRuleCondition = IPushRuleCondition<string>
|
||||
| IEventMatchCondition
|
||||
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition = IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
|
||||
@@ -168,6 +168,11 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
|
||||
"m.in_thread",
|
||||
"io.element.in_thread",
|
||||
);
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
Vendored
+8
-20
@@ -33,11 +33,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron?: Electron;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
webkitAudioContext: typeof AudioContext;
|
||||
}
|
||||
|
||||
interface Crypto {
|
||||
@@ -66,21 +62,6 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
}
|
||||
|
||||
interface GetSourcesOptions {
|
||||
types: Array<string>;
|
||||
thumbnailSize?: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
fetchWindowIcons?: boolean;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
@@ -107,4 +88,11 @@ declare global {
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
interface RTCRtpTransceiver {
|
||||
// This has been removed from TS
|
||||
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
|
||||
// but we still need this for MatrixCall::getRidOfRTXCodecs()
|
||||
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -55,12 +55,20 @@ export enum JoinRule {
|
||||
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
|
||||
*/
|
||||
Private = "private",
|
||||
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
|
||||
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
Knock = "knock",
|
||||
Restricted = "restricted",
|
||||
}
|
||||
|
||||
export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
RoomMembership = "m.room_membership",
|
||||
}
|
||||
|
||||
export interface IJoinRuleEventContent {
|
||||
join_rule: JoinRule; // eslint-disable-line camelcase
|
||||
allow?: {
|
||||
type: RestrictedAllowType;
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
@@ -74,11 +82,3 @@ export enum HistoryVisibility {
|
||||
Shared = "shared",
|
||||
WorldReadable = "world_readable",
|
||||
}
|
||||
|
||||
// XXX move to OlmDevice when converted
|
||||
export interface InboundGroupSessionData {
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
session: string;
|
||||
keysClaimed: Record<string, string>;
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
}
|
||||
|
||||
@@ -15,21 +15,26 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { IPublicRoomsChunkRoom } from "../client";
|
||||
import { RoomType } from "./event";
|
||||
import { IStrippedState } from "../sync-accumulator";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
sender: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
@@ -37,4 +42,19 @@ export interface ISpaceSummaryEvent {
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
room_id: string;
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
|
||||
room_type?: RoomType | string;
|
||||
children_state: IHierarchyRelation[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -17,79 +17,19 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Description for what an automatically discovered client configuration
|
||||
* would look like. Although this is a class, it is recommended that it
|
||||
* be treated as an interface definition rather than as a class.
|
||||
*
|
||||
* Additional properties than those defined here may be present, and
|
||||
* should follow the Java package naming convention.
|
||||
*/
|
||||
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
|
||||
// Dev note: this is basically a copy/paste of the .well-known response
|
||||
// object as defined in the spec. It does have additional information,
|
||||
// however. Overall, this exists to serve as a place for documentation
|
||||
// and not functionality.
|
||||
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* The homeserver configuration the client should use. This will
|
||||
* always be present on the object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.homeserver"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
|
||||
* if this is not AutoDiscovery.SUCCESS then the client should
|
||||
* assume the other properties in the client config (such as
|
||||
* the identity server configuration) are not valid.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
|
||||
* then this will contain a human-readable (English) message
|
||||
* for what went wrong. If the state is none of those previously
|
||||
* mentioned, this will be falsey.
|
||||
*/
|
||||
error: "Something went wrong",
|
||||
|
||||
/**
|
||||
* The base URL clients should use to talk to the homeserver,
|
||||
* particularly for the login process. May be falsey if the
|
||||
* state is not AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://matrix.org",
|
||||
};
|
||||
|
||||
/**
|
||||
* The identity server configuration the client should use. This
|
||||
* will always be present on teh object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.identity_server"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* The base URL clients should use for interacting with the
|
||||
* identity server. May be falsey if the state is not
|
||||
* AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://vector.im",
|
||||
};
|
||||
}
|
||||
export enum AutoDiscoveryAction {
|
||||
SUCCESS = "SUCCESS",
|
||||
IGNORE = "IGNORE",
|
||||
PROMPT = "PROMPT",
|
||||
FAIL_PROMPT = "FAIL_PROMPT",
|
||||
FAIL_ERROR = "FAIL_ERROR",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,55 +42,36 @@ export class AutoDiscovery {
|
||||
// translate the meaning of the states in the spec, but also
|
||||
// support our own if needed.
|
||||
|
||||
static get ERROR_INVALID() {
|
||||
return "Invalid homeserver discovery response";
|
||||
}
|
||||
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
|
||||
|
||||
static get ERROR_GENERIC_FAILURE() {
|
||||
return "Failed to get autodiscovery configuration from server";
|
||||
}
|
||||
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
|
||||
|
||||
static get ERROR_INVALID_HS_BASE_URL() {
|
||||
return "Invalid base_url for m.homeserver";
|
||||
}
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
|
||||
|
||||
static get ERROR_INVALID_HOMESERVER() {
|
||||
return "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
}
|
||||
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
|
||||
static get ERROR_INVALID_IS_BASE_URL() {
|
||||
return "Invalid base_url for m.identity_server";
|
||||
}
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
|
||||
|
||||
static get ERROR_INVALID_IDENTITY_SERVER() {
|
||||
return "Identity server URL does not appear to be a valid identity server";
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
|
||||
|
||||
static get ERROR_INVALID_IS() {
|
||||
return "Invalid identity server discovery response";
|
||||
}
|
||||
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
|
||||
|
||||
static get ERROR_MISSING_WELLKNOWN() {
|
||||
return "No .well-known JSON file found";
|
||||
}
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
|
||||
|
||||
static get ERROR_INVALID_JSON() {
|
||||
return "Invalid JSON";
|
||||
}
|
||||
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
|
||||
|
||||
static get ALL_ERRORS() {
|
||||
return [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
}
|
||||
public static readonly ALL_ERRORS = [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
@@ -158,7 +79,7 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_ERROR() { return "FAIL_ERROR"; }
|
||||
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
/**
|
||||
* The auto discovery failed, however the client may still recover
|
||||
@@ -169,7 +90,7 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
|
||||
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
|
||||
|
||||
/**
|
||||
* The auto discovery didn't fail but did not find anything of
|
||||
@@ -178,14 +99,14 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get PROMPT() { return "PROMPT"; }
|
||||
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
|
||||
|
||||
/**
|
||||
* The auto discovery was successful.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get SUCCESS() { return "SUCCESS"; }
|
||||
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
|
||||
|
||||
/**
|
||||
* Validates and verifies client configuration information for purposes
|
||||
@@ -193,13 +114,13 @@ export class AutoDiscovery {
|
||||
* and identity server URL the client would want. Additional details
|
||||
* may also be included, and will be transparently brought into the
|
||||
* response object unaltered.
|
||||
* @param {string} wellknown The configuration object itself, as returned
|
||||
* @param {object} wellknown The configuration object itself, as returned
|
||||
* by the .well-known auto-discovery endpoint.
|
||||
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
static async fromDiscoveryConfig(wellknown) {
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
@@ -240,7 +161,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
|
||||
// sure it points to a homeserver in Step 3.
|
||||
const hsUrl = this._sanitizeWellKnownUrl(
|
||||
const hsUrl = this.sanitizeWellKnownUrl(
|
||||
wellknown["m.homeserver"]["base_url"],
|
||||
);
|
||||
if (!hsUrl) {
|
||||
@@ -250,7 +171,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this._fetchWellKnownObject(
|
||||
const hsVersions = await this.fetchWellKnownObject(
|
||||
`${hsUrl}/_matrix/client/versions`,
|
||||
);
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
@@ -272,7 +193,7 @@ export class AutoDiscovery {
|
||||
};
|
||||
|
||||
// Step 5: Try to pull out the identity server configuration
|
||||
let isUrl = "";
|
||||
let isUrl: string | boolean = "";
|
||||
if (wellknown["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch.
|
||||
@@ -287,7 +208,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
|
||||
// points to an identity server in Step 5b.
|
||||
isUrl = this._sanitizeWellKnownUrl(
|
||||
isUrl = this.sanitizeWellKnownUrl(
|
||||
wellknown["m.identity_server"]["base_url"],
|
||||
);
|
||||
if (!isUrl) {
|
||||
@@ -299,10 +220,10 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 5b: Verify there is an identity server listening on the provided
|
||||
// URL.
|
||||
const isResponse = await this._fetchWellKnownObject(
|
||||
const isResponse = await this.fetchWellKnownObject(
|
||||
`${isUrl}/_matrix/identity/api/v1`,
|
||||
);
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
failingClientConfig["m.identity_server"].error =
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
||||
@@ -317,7 +238,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 6: Now that the identity server is valid, or never existed,
|
||||
// populate the IS section.
|
||||
if (isUrl && isUrl.length > 0) {
|
||||
if (isUrl && isUrl.toString().length > 0) {
|
||||
clientConfig["m.identity_server"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
@@ -359,7 +280,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when discovery fails.
|
||||
*/
|
||||
static async findClientConfig(domain) {
|
||||
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
@@ -395,13 +316,13 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 1: Actually request the .well-known JSON file and make sure it
|
||||
// at least has a homeserver definition.
|
||||
const wellknown = await this._fetchWellKnownObject(
|
||||
const wellknown = await this.fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!wellknown || wellknown.action !== "SUCCESS") {
|
||||
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("No response or error when parsing .well-known");
|
||||
if (wellknown.reason) logger.error(wellknown.reason);
|
||||
if (wellknown.action === "IGNORE") {
|
||||
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.PROMPT,
|
||||
error: null,
|
||||
@@ -427,12 +348,12 @@ export class AutoDiscovery {
|
||||
* @returns {Promise<object>} Resolves to the domain's client config. Can
|
||||
* be an empty object.
|
||||
*/
|
||||
static async getRawClientConfig(domain) {
|
||||
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
|
||||
const response = await this._fetchWellKnownObject(
|
||||
const response = await this.fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!response) return {};
|
||||
@@ -447,7 +368,7 @@ export class AutoDiscovery {
|
||||
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
||||
* @private
|
||||
*/
|
||||
static _sanitizeWellKnownUrl(url) {
|
||||
private static sanitizeWellKnownUrl(url: string): string | boolean {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
@@ -495,8 +416,9 @@ export class AutoDiscovery {
|
||||
* @return {Promise<object>} Resolves to the returned state.
|
||||
* @private
|
||||
*/
|
||||
static async _fetchWellKnownObject(url) {
|
||||
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// eslint-disable-next-line
|
||||
const request = require("./matrix").getRequest();
|
||||
if (!request) throw new Error("No request library available");
|
||||
request(
|
||||
@@ -505,10 +427,10 @@ export class AutoDiscovery {
|
||||
if (err || response &&
|
||||
(response.statusCode < 200 || response.statusCode >= 300)
|
||||
) {
|
||||
let action = "FAIL_PROMPT";
|
||||
let action = AutoDiscoveryAction.FAIL_PROMPT;
|
||||
let reason = (err ? err.message : null) || "General failure";
|
||||
if (response && response.statusCode === 404) {
|
||||
action = "IGNORE";
|
||||
action = AutoDiscoveryAction.IGNORE;
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
resolve({ raw: {}, action: action, reason: reason, error: err });
|
||||
@@ -516,7 +438,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
try {
|
||||
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
|
||||
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
|
||||
} catch (e) {
|
||||
let reason = AutoDiscovery.ERROR_INVALID;
|
||||
if (e.name === "SyntaxError") {
|
||||
@@ -524,7 +446,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
resolve({
|
||||
raw: {},
|
||||
action: "FAIL_PROMPT",
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: reason,
|
||||
error: e,
|
||||
});
|
||||
+137
-39
@@ -30,10 +30,11 @@ import * as utils from './utils';
|
||||
import { sleep } from './utils';
|
||||
import { Group } from "./models/group";
|
||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||
import { PushAction, PushProcessor } from "./pushprocessor";
|
||||
import { AutoDiscovery } from "./autodiscovery";
|
||||
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||
import * as olmlib from "./crypto/olmlib";
|
||||
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
|
||||
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
|
||||
import { ReEmitter } from './ReEmitter';
|
||||
import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
||||
import { logger } from './logger';
|
||||
@@ -74,7 +75,6 @@ import {
|
||||
IKeyBackupPrepareOpts,
|
||||
IKeyBackupRestoreOpts,
|
||||
IKeyBackupRestoreResult,
|
||||
IKeyBackupSession,
|
||||
} from "./crypto/keybackup";
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import type Request from "request";
|
||||
@@ -92,7 +92,7 @@ import {
|
||||
import { SyncState } from "./sync.api";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
|
||||
import { Base as Verification } from "./crypto/verification/Base";
|
||||
import { VerificationBase as Verification } from "./crypto/verification/Base";
|
||||
import * as ContentHelpers from "./content-helpers";
|
||||
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
|
||||
import { Room } from "./models/room";
|
||||
@@ -140,10 +140,11 @@ import {
|
||||
SearchOrderBy,
|
||||
} from "./@types/search";
|
||||
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
|
||||
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
|
||||
import { IHierarchyRoom, ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
|
||||
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
|
||||
import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
|
||||
export type Store = IStore;
|
||||
export type SessionStore = WebStorageSessionStore;
|
||||
@@ -156,12 +157,6 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||
|
||||
interface IOlmDevice {
|
||||
pickledAccount: string;
|
||||
sessions: Array<Record<string, IKeyBackupSession>>;
|
||||
pickleKey: string;
|
||||
}
|
||||
|
||||
interface IExportedDevice {
|
||||
olmDevice: IOlmDevice;
|
||||
userId: string;
|
||||
@@ -380,6 +375,11 @@ export interface IStartClientOpts {
|
||||
* This should be in the order of hours. Default: undefined.
|
||||
*/
|
||||
clientWellKnownPollPeriod?: number;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
experimentalThreadSupport?: boolean;
|
||||
}
|
||||
|
||||
export interface IStoredClientOpts extends IStartClientOpts {
|
||||
@@ -476,14 +476,19 @@ interface IServerVersions {
|
||||
unstable_features: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface IClientWellKnown {
|
||||
export interface IClientWellKnown {
|
||||
[key: string]: any;
|
||||
"m.homeserver": {
|
||||
base_url: string;
|
||||
};
|
||||
"m.identity_server"?: {
|
||||
base_url: string;
|
||||
};
|
||||
"m.homeserver"?: IWellKnownConfig;
|
||||
"m.identity_server"?: IWellKnownConfig;
|
||||
}
|
||||
|
||||
export interface IWellKnownConfig {
|
||||
raw?: any; // todo typings
|
||||
action?: AutoDiscoveryAction;
|
||||
reason?: string;
|
||||
error?: Error | string;
|
||||
// eslint-disable-next-line
|
||||
base_url?: string | null;
|
||||
}
|
||||
|
||||
interface IKeyBackupPath {
|
||||
@@ -521,7 +526,7 @@ interface IMessagesResponse {
|
||||
state: IStateEvent[];
|
||||
}
|
||||
|
||||
interface IRequestTokenResponse {
|
||||
export interface IRequestTokenResponse {
|
||||
sid: string;
|
||||
submit_url?: string;
|
||||
}
|
||||
@@ -600,7 +605,7 @@ interface IUserDirectoryResponse {
|
||||
limited: boolean;
|
||||
}
|
||||
|
||||
interface IMyDevice {
|
||||
export interface IMyDevice {
|
||||
device_id: string;
|
||||
display_name?: string;
|
||||
last_seen_ip?: string;
|
||||
@@ -673,7 +678,7 @@ export class MatrixClient extends EventEmitter {
|
||||
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
||||
|
||||
public reEmitter = new ReEmitter(this);
|
||||
public olmVersion: string = null; // populated after initCrypto
|
||||
public olmVersion: [number, number, number] = null; // populated after initCrypto
|
||||
public usingExternalCrypto = false;
|
||||
public store: Store;
|
||||
public deviceId?: string;
|
||||
@@ -734,6 +739,7 @@ export class MatrixClient extends EventEmitter {
|
||||
protected checkTurnServersIntervalID: number;
|
||||
protected exportedOlmDeviceToImport: IOlmDevice;
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler();
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
super();
|
||||
@@ -1241,6 +1247,13 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.canSupportVoip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {MediaHandler}
|
||||
*/
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether VoIP calls are forced to use only TURN
|
||||
* candidates. This is the same as the forceTURN option
|
||||
@@ -2028,7 +2041,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
public createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
|
||||
public createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2471,7 +2484,7 @@ export class MatrixClient extends EventEmitter {
|
||||
*/
|
||||
// TODO: Verify types
|
||||
public async prepareKeyBackupVersion(
|
||||
password: string,
|
||||
password?: string,
|
||||
opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
|
||||
): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
|
||||
if (!this.crypto) {
|
||||
@@ -2931,6 +2944,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* has been emitted.
|
||||
* @param {string} groupId The group ID
|
||||
* @return {Group} The Group or null if the group is not known or there is no data store.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroup(groupId: string): Group {
|
||||
return this.store.getGroup(groupId);
|
||||
@@ -2939,6 +2953,7 @@ export class MatrixClient extends EventEmitter {
|
||||
/**
|
||||
* Retrieve all known groups.
|
||||
* @return {Group[]} A list of groups, or an empty list if there is no data store.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroups(): Group[] {
|
||||
return this.store.getGroups();
|
||||
@@ -4340,7 +4355,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {MatrixEvent} event The event to get push actions for.
|
||||
* @return {module:pushprocessor~PushAction} A dict of actions to perform.
|
||||
*/
|
||||
public getPushActionsForEvent(event: MatrixEvent): PushAction {
|
||||
public getPushActionsForEvent(event: MatrixEvent): IActionsObject {
|
||||
if (!event.getPushActions()) {
|
||||
event.setPushActions(this.pushProcessor.actionsForEvent(event));
|
||||
}
|
||||
@@ -5076,7 +5091,7 @@ export class MatrixClient extends EventEmitter {
|
||||
email: string,
|
||||
clientSecret: string,
|
||||
sendAttempt: number,
|
||||
nextLink: string,
|
||||
nextLink?: string,
|
||||
): Promise<IRequestTokenResponse> {
|
||||
return this.requestTokenFromEndpoint(
|
||||
"/account/password/email/requestToken",
|
||||
@@ -6106,17 +6121,17 @@ export class MatrixClient extends EventEmitter {
|
||||
public register(
|
||||
username: string,
|
||||
password: string,
|
||||
sessionId: string,
|
||||
auth: any,
|
||||
bindThreepids: any,
|
||||
guestAccessToken: string,
|
||||
inhibitLogin: boolean,
|
||||
sessionId: string | null,
|
||||
auth: { session?: string, type: string },
|
||||
bindThreepids?: boolean | null | { email?: boolean, msisdn?: boolean },
|
||||
guestAccessToken?: string,
|
||||
inhibitLogin?: boolean,
|
||||
callback?: Callback,
|
||||
): Promise<any> { // TODO: Types (many)
|
||||
// backwards compat
|
||||
if (bindThreepids === true) {
|
||||
bindThreepids = { email: true };
|
||||
} else if (bindThreepids === null || bindThreepids === undefined) {
|
||||
} else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
|
||||
bindThreepids = {};
|
||||
}
|
||||
if (typeof inhibitLogin === 'function') {
|
||||
@@ -7450,7 +7465,7 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.authedRequest(undefined, "GET", path, qps, undefined);
|
||||
}
|
||||
|
||||
public uploadDeviceSigningKeys(auth: any, keys: CrossSigningKeys): Promise<{}> { // TODO: types
|
||||
public uploadDeviceSigningKeys(auth: any, keys?: CrossSigningKeys): Promise<{}> { // TODO: types
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, { auth });
|
||||
return this.http.authedRequest(
|
||||
@@ -7973,14 +7988,15 @@ export class MatrixClient extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches or paginates a summary of a space as defined by MSC2946
|
||||
* Fetches or paginates a summary of a space as defined by an initial version of MSC2946
|
||||
* @param {string} roomId The ID of the space-room to use as the root of the summary.
|
||||
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
|
||||
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
|
||||
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
|
||||
* @param {number?} limit The maximum number of rooms to return in total.
|
||||
* @param {string?} batch The opaque token to paginate a previous summary request.
|
||||
* @returns {Promise} the response, with next_batch, rooms, events fields.
|
||||
* @returns {Promise} the response, with next_token, rooms fields.
|
||||
* @deprecated in favour of `getRoomHierarchy` due to the MSC changing paths.
|
||||
*/
|
||||
public getSpaceSummary(
|
||||
roomId: string,
|
||||
@@ -7989,10 +8005,7 @@ export class MatrixClient extends EventEmitter {
|
||||
autoJoinOnly?: boolean,
|
||||
limit?: number,
|
||||
batch?: string,
|
||||
): Promise<{
|
||||
rooms: ISpaceSummaryRoom[];
|
||||
events: ISpaceSummaryEvent[];
|
||||
}> {
|
||||
): Promise<{rooms: ISpaceSummaryRoom[], events: ISpaceSummaryEvent[]}> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/spaces", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
@@ -8008,6 +8021,60 @@ export class MatrixClient extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches or paginates a room hierarchy as defined by MSC2946.
|
||||
* Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
|
||||
* @param {string} roomId The ID of the space-room to use as the root of the summary.
|
||||
* @param {number?} limit The maximum number of rooms to return per page.
|
||||
* @param {number?} maxDepth The maximum depth in the tree from the root room to return.
|
||||
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
|
||||
* @param {string?} fromToken The opaque token to paginate a previous request.
|
||||
* @returns {Promise} the response, with next_batch & rooms fields.
|
||||
*/
|
||||
public getRoomHierarchy(
|
||||
roomId: string,
|
||||
limit?: number,
|
||||
maxDepth?: number,
|
||||
suggestedOnly = false,
|
||||
fromToken?: string,
|
||||
): Promise<{
|
||||
rooms: IHierarchyRoom[];
|
||||
next_batch?: string; // eslint-disable-line camelcase
|
||||
}> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/hierarchy", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
return this.http.authedRequest(undefined, "GET", path, {
|
||||
suggested_only: suggestedOnly,
|
||||
max_depth: maxDepth,
|
||||
from: fromToken,
|
||||
limit,
|
||||
}, undefined, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
}).catch(e => {
|
||||
if (e.errcode === "M_UNRECOGNIZED") {
|
||||
// fall back to the older space summary API as it exposes the same data just in a different shape.
|
||||
return this.getSpaceSummary(roomId, undefined, suggestedOnly, undefined, limit)
|
||||
.then(({ rooms, events }) => {
|
||||
// Translate response from `/spaces` to that we expect in this API.
|
||||
const roomMap = new Map(rooms.map(r => {
|
||||
return [r.room_id, <IHierarchyRoom>{ ...r, children_state: [] }];
|
||||
}));
|
||||
events.forEach(e => {
|
||||
roomMap.get(e.room_id)?.children_state.push(e);
|
||||
});
|
||||
|
||||
return {
|
||||
rooms: Array.from(roomMap.values()),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file tree space with the given name. The client will pick
|
||||
* defaults for how it expects to be able to support the remaining API offered
|
||||
@@ -8060,7 +8127,7 @@ export class MatrixClient extends EventEmitter {
|
||||
*/
|
||||
public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace {
|
||||
const room = this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room?.getMyMembership() !== 'join') return null;
|
||||
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
const purposeEvent = room.currentState.getStateEvents(
|
||||
@@ -8085,6 +8152,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Group summary object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroupSummary(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
|
||||
@@ -8095,6 +8163,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Group profile object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroupProfile(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
|
||||
@@ -8110,6 +8179,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string=} profile.long_description A longer HTML description of the room
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public setGroupProfile(groupId: string, profile: any): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
|
||||
@@ -8126,6 +8196,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* required to join.
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8143,6 +8214,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Group users list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroupUsers(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
|
||||
@@ -8153,6 +8225,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Group users list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroupInvitedUsers(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
|
||||
@@ -8163,6 +8236,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Group rooms list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroupRooms(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
|
||||
@@ -8174,6 +8248,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} userId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public inviteUserToGroup(groupId: string, userId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8188,6 +8263,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} userId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public removeUserFromGroup(groupId: string, userId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8203,6 +8279,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} roleId Optional.
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8219,6 +8296,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} userId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8234,6 +8312,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} categoryId Optional.
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8250,6 +8329,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} roomId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8265,6 +8345,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {boolean} isPublic Whether the room-group association is visible to non-members
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
|
||||
if (isPublic === undefined) {
|
||||
@@ -8286,6 +8367,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {boolean} isPublic Whether the room-group association is visible to non-members
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
|
||||
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
|
||||
@@ -8306,6 +8388,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} roomId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8320,6 +8403,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {Object} opts Additional options to send alongside the acceptance.
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public acceptGroupInvite(groupId: string, opts = null): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8333,6 +8417,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public joinGroup(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8346,6 +8431,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {string} groupId
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public leaveGroup(groupId: string): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8358,6 +8444,7 @@ export class MatrixClient extends EventEmitter {
|
||||
/**
|
||||
* @return {Promise} Resolves: The groups to which the user is joined
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getJoinedGroups(): Promise<any> {
|
||||
const path = utils.encodeUri("/joined_groups", {});
|
||||
@@ -8370,6 +8457,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {Object} content.profile Group profile object
|
||||
* @return {Promise} Resolves: Object with key group_id: id of the created group
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public createGroup(content: any): Promise<any> {
|
||||
const path = utils.encodeUri("/create_group", {});
|
||||
@@ -8390,6 +8478,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* }
|
||||
* }
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getPublicisedGroups(userIds: string[]): Promise<any> {
|
||||
const path = utils.encodeUri("/publicised_groups", {});
|
||||
@@ -8403,6 +8492,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {boolean} isPublic Whether the user's membership of this group is made public
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> {
|
||||
const path = utils.encodeUri(
|
||||
@@ -8413,6 +8503,13 @@ export class MatrixClient extends EventEmitter {
|
||||
publicise: isPublic,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
return this.clientOpts?.experimentalThreadSupport || false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8567,6 +8664,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* is experimental and may change.</strong>
|
||||
* @event module:client~MatrixClient#"Group"
|
||||
* @param {Group} group The newly created, fully populated group.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
* @example
|
||||
* matrixClient.on("Group", function(group){
|
||||
* var groupId = group.groupId;
|
||||
|
||||
@@ -713,7 +713,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
});
|
||||
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, type);
|
||||
return decodeBase64(decrypted);
|
||||
} else {
|
||||
@@ -726,7 +726,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||
);
|
||||
}
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
@@ -739,6 +739,8 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
};
|
||||
}
|
||||
|
||||
export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
|
||||
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
@@ -746,15 +748,19 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) {
|
||||
export function requestKeysDuringVerification(
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<KeysDuringVerification | void> {
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (baseApis.getUserId() !== userId) {
|
||||
return;
|
||||
}
|
||||
logger.log("Cross-signing: Self-verification done; requesting keys");
|
||||
// This happens asynchronously, and we're not concerned about waiting for
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
// it. We return here in order to test.
|
||||
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client.crypto.crossSigningInfo;
|
||||
|
||||
@@ -781,7 +787,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
// https://github.com/vector-im/element-web/issues/12604
|
||||
// then change here to reject on the timeout
|
||||
// Requests can be ignored, so don't wait around forever
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
const timeout = new Promise<void>((resolve) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
KEY_REQUEST_TIMEOUT_MS,
|
||||
@@ -801,7 +807,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client.crypto.storeSessionBackupPrivateKey(
|
||||
await client.crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
@@ -814,13 +820,13 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
})();
|
||||
|
||||
// We call getCrossSigningKey() for its side-effects
|
||||
return Promise.race([
|
||||
return Promise.race<KeysDuringVerification | void>([
|
||||
Promise.all([
|
||||
crossSigning.getCrossSigningKey("master"),
|
||||
crossSigning.getCrossSigningKey("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
]) as Promise<KeysDuringVerification>,
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
|
||||
@@ -59,7 +59,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks: ICryptoCallbacks) {
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
@@ -351,12 +351,12 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
class SSSSCryptoCallbacks {
|
||||
private readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
||||
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
name: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
): Promise<[string, Uint8Array]|null> {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this.privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
@@ -365,7 +365,7 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this.delegateCryptoCallbacks) {
|
||||
if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
|
||||
const result = await this.delegateCryptoCallbacks.
|
||||
getSecretStorageKey({ keys }, name);
|
||||
if (result) {
|
||||
@@ -374,6 +374,7 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -26,9 +26,10 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
const zeroSalt = new Uint8Array(8);
|
||||
|
||||
export interface IEncryptedPayload {
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
mac: string;
|
||||
[key: string]: any; // extensible
|
||||
iv?: string;
|
||||
ciphertext?: string;
|
||||
mac?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface IOlmDevice<T = DeviceInfo> {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IOutboundGroupSessionKey {
|
||||
export interface IOutboundGroupSessionKey {
|
||||
chain_index: number;
|
||||
key: string;
|
||||
}
|
||||
@@ -101,6 +101,13 @@ interface IPayload extends Partial<IMessage> {
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
// The identity key of the device we shared with
|
||||
deviceKey: string;
|
||||
// The message index of the ratchet we shared with that device
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
@@ -115,12 +122,12 @@ interface IPayload extends Partial<IMessage> {
|
||||
*
|
||||
* @property {object} sharedWithDevices
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
* userId -> {deviceId -> SharedWithData}
|
||||
*/
|
||||
class OutboundSessionInfo {
|
||||
public useCount = 0;
|
||||
public creationTime: number;
|
||||
public sharedWithDevices: Record<string, Record<string, number>> = {};
|
||||
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
|
||||
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
|
||||
|
||||
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
|
||||
@@ -150,11 +157,11 @@ class OutboundSessionInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
public markSharedWithDevice(userId: string, deviceId: string, chainIndex: number): void {
|
||||
public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
|
||||
if (!this.sharedWithDevices[userId]) {
|
||||
this.sharedWithDevices[userId] = {};
|
||||
}
|
||||
this.sharedWithDevices[userId][deviceId] = chainIndex;
|
||||
this.sharedWithDevices[userId][deviceId] = { deviceKey, messageIndex: chainIndex };
|
||||
}
|
||||
|
||||
public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
|
||||
@@ -572,6 +579,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
payload: IPayload,
|
||||
): Promise<void> {
|
||||
const contentMap = {};
|
||||
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < userDeviceMap.length; i++) {
|
||||
@@ -584,6 +592,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const userId = val.userId;
|
||||
const deviceInfo = val.deviceInfo;
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
deviceInfoByDeviceId.set(deviceId, deviceInfo);
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
@@ -636,7 +645,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
session.markSharedWithDevice(
|
||||
userId, deviceId, chainIndex,
|
||||
userId,
|
||||
deviceId,
|
||||
deviceInfoByDeviceId.get(deviceId).getIdentityKey(),
|
||||
chainIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -719,8 +731,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
|
||||
return;
|
||||
}
|
||||
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sentChainIndex === undefined) {
|
||||
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sessionSharedData === undefined) {
|
||||
logger.debug(
|
||||
"megolm session ID " + sessionId + " never shared with device " +
|
||||
userId + ":" + device.deviceId,
|
||||
@@ -728,10 +740,18 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
|
||||
logger.warn(
|
||||
`Session has been shared with device ${device.deviceId} but with identity ` +
|
||||
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the key from the inbound session: the outbound one will already
|
||||
// have been ratcheted to the next chain index.
|
||||
const key = await this.olmDevice.getInboundGroupSessionKey(
|
||||
this.roomId, senderKey, sessionId, sentChainIndex,
|
||||
this.roomId, senderKey, sessionId, sessionSharedData.messageIndex,
|
||||
);
|
||||
|
||||
if (!key) {
|
||||
@@ -882,14 +902,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
|
||||
session.markSharedWithDevice(
|
||||
userId, deviceId, key.chain_index,
|
||||
userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index,
|
||||
);
|
||||
}
|
||||
|
||||
const filteredFailedDevices =
|
||||
await this.olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this.roomId}`,
|
||||
@@ -1391,7 +1409,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
public onRoomKeyEvent(event: MatrixEvent): void {
|
||||
public onRoomKeyEvent(event: MatrixEvent): Promise<void> {
|
||||
const content = event.getContent();
|
||||
const sessionId = content.session_id;
|
||||
let senderKey = event.getSenderKey();
|
||||
|
||||
@@ -36,7 +36,7 @@ import { IEventDecryptionResult } from "../index";
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
interface IMessage {
|
||||
type: number | string;
|
||||
type: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
@@ -269,11 +269,11 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
// not a prekey message: we can safely just try & decrypt it
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
} else {
|
||||
const myPromise = this.olmDevice._olmPrekeyPromise.then(() => {
|
||||
const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
});
|
||||
// we want the error, but don't propagate it to the next decryption
|
||||
this.olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
|
||||
return await myPromise;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ export interface IEncryptedEventInfo {
|
||||
}
|
||||
|
||||
export interface IRecoveryKey {
|
||||
keyInfo: {
|
||||
keyInfo?: {
|
||||
pubkey: string;
|
||||
passphrase?: {
|
||||
algorithm: string;
|
||||
@@ -67,7 +67,7 @@ export interface IRecoveryKey {
|
||||
};
|
||||
};
|
||||
privateKey: Uint8Array;
|
||||
encodedPrivateKey: string;
|
||||
encodedPrivateKey?: string;
|
||||
}
|
||||
|
||||
export interface ICreateSecretStorageOpts {
|
||||
|
||||
@@ -20,6 +20,7 @@ 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>>;
|
||||
@@ -60,7 +61,7 @@ export class DehydrationManager {
|
||||
private key: Uint8Array;
|
||||
private keyInfo: {[props: string]: any};
|
||||
private deviceDisplayName: string;
|
||||
constructor(private crypto) {
|
||||
constructor(private readonly crypto: Crypto) {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
@@ -73,7 +74,7 @@ export class DehydrationManager {
|
||||
async (result) => {
|
||||
if (result) {
|
||||
const { key, keyInfo, deviceDisplayName, time } = result;
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.key = decodeBase64(decrypted);
|
||||
this.keyInfo = keyInfo;
|
||||
@@ -158,7 +159,7 @@ export class DehydrationManager {
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||
|
||||
// update the crypto store with the timestamp
|
||||
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DeviceInfo {
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo} new DeviceInfo
|
||||
*/
|
||||
public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo {
|
||||
public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
|
||||
const res = new DeviceInfo(deviceId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
|
||||
+36
-27
@@ -26,7 +26,7 @@ import { EventEmitter } from 'events';
|
||||
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { logger } from '../logger';
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
||||
import * as olmlib from "./olmlib";
|
||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
@@ -66,6 +66,7 @@ import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { IVerificationChannel } from "./verification/request/Channel";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -84,12 +85,12 @@ const defaultVerificationMethods = {
|
||||
* verification method names
|
||||
*/
|
||||
// legacy export identifier
|
||||
export enum verificationMethods {
|
||||
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
|
||||
SAS = SASVerification.NAME,
|
||||
}
|
||||
export const verificationMethods = {
|
||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
||||
SAS: SASVerification.NAME,
|
||||
};
|
||||
|
||||
export type VerificationMethod = verificationMethods;
|
||||
export type VerificationMethod = keyof typeof verificationMethods | string;
|
||||
|
||||
export function isCryptoAvailable(): boolean {
|
||||
return Boolean(global.Olm);
|
||||
@@ -98,7 +99,7 @@ export function isCryptoAvailable(): boolean {
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
|
||||
interface IInitOpts {
|
||||
exportedOlmDevice?: any; // TODO types
|
||||
exportedOlmDevice?: IExportedDevice;
|
||||
pickleKey?: string;
|
||||
}
|
||||
|
||||
@@ -130,13 +131,14 @@ export interface IRoomKeyRequestBody extends IRoomKey {
|
||||
}
|
||||
|
||||
export interface IMegolmSessionData {
|
||||
[key: string]: any; // extensible
|
||||
sender_key: string;
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_keys: Record<string, string>;
|
||||
room_id: string;
|
||||
session_id: string;
|
||||
session_key: string;
|
||||
algorithm: string;
|
||||
algorithm?: string;
|
||||
untrusted?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
@@ -192,7 +194,7 @@ export class Crypto extends EventEmitter {
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
static getOlmVersion(): string {
|
||||
static getOlmVersion(): [number, number, number] {
|
||||
return OlmDevice.getOlmVersion();
|
||||
}
|
||||
|
||||
@@ -205,7 +207,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly verificationMethods: any; // TODO types
|
||||
private readonly supportedAlgorithms: string[];
|
||||
public readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
public readonly inRoomVerificationRequests: InRoomRequests;
|
||||
@@ -291,7 +293,7 @@ export class Crypto extends EventEmitter {
|
||||
constructor(
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly sessionStore: SessionStore,
|
||||
private readonly userId: string,
|
||||
public readonly userId: string,
|
||||
private readonly deviceId: string,
|
||||
private readonly clientStore: IStore,
|
||||
public readonly cryptoStore: CryptoStore,
|
||||
@@ -504,7 +506,7 @@ export class Crypto extends EventEmitter {
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
public async createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
|
||||
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
|
||||
@@ -1097,7 +1099,7 @@ export class Crypto extends EventEmitter {
|
||||
await this.storeSessionBackupPrivateKey(key);
|
||||
}
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
|
||||
key = olmlib.decodeBase64(decrypted);
|
||||
}
|
||||
@@ -1113,7 +1115,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||
}
|
||||
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
||||
return this.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
@@ -1912,7 +1914,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||
if (this.getNeedsNewFallback()) {
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||
const k = { key, fallback: true };
|
||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||
@@ -2176,11 +2178,11 @@ export class Crypto extends EventEmitter {
|
||||
return this.inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
}
|
||||
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest {
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
|
||||
return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
|
||||
}
|
||||
|
||||
public requestVerificationDM(userId: string, roomId: string): VerificationRequest {
|
||||
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||
const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
if (existingRequest) {
|
||||
return Promise.resolve(existingRequest);
|
||||
@@ -2193,7 +2195,7 @@ export class Crypto extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
public requestVerification(userId: string, devices: string[]): VerificationRequest {
|
||||
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
|
||||
if (!devices) {
|
||||
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
|
||||
}
|
||||
@@ -2211,9 +2213,9 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private async requestVerificationWithChannel(
|
||||
userId: string,
|
||||
channel: any, // TODO types
|
||||
channel: IVerificationChannel,
|
||||
requestsMap: any, // TODO types
|
||||
): VerificationRequest {
|
||||
): Promise<VerificationRequest> {
|
||||
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
|
||||
// if transaction id is already known, add request
|
||||
if (channel.transactionId) {
|
||||
@@ -2259,14 +2261,11 @@ export class Crypto extends EventEmitter {
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
method: VerificationMethod,
|
||||
): VerificationRequest {
|
||||
): Promise<VerificationRequest> {
|
||||
const transactionId = ToDeviceChannel.makeTransactionId();
|
||||
const channel = new ToDeviceChannel(
|
||||
this.baseApis, userId, [deviceId], transactionId, deviceId);
|
||||
const request = new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(
|
||||
userId, transactionId, request);
|
||||
const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
|
||||
const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
|
||||
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
|
||||
const verifier = request.beginKeyVerification(method, { userId, deviceId });
|
||||
// either reject by an error from verify() while sending .start
|
||||
// or resolve when the request receives the
|
||||
@@ -2757,12 +2756,22 @@ export class Crypto extends EventEmitter {
|
||||
delete content['m.relates_to'];
|
||||
}
|
||||
|
||||
// Treat element's performance metrics the same as `m.relates_to` (when present)
|
||||
const elementPerfMetrics = content['io.element.performance_metrics'];
|
||||
if (elementPerfMetrics) {
|
||||
content = Object.assign({}, content);
|
||||
delete content['io.element.performance_metrics'];
|
||||
}
|
||||
|
||||
const encryptedContent = await alg.encryptMessage(
|
||||
room, event.getType(), content);
|
||||
|
||||
if (mRelatesTo) {
|
||||
encryptedContent['m.relates_to'] = mRelatesTo;
|
||||
}
|
||||
if (elementPerfMetrics) {
|
||||
encryptedContent['io.element.performance_metrics'] = elementPerfMetrics;
|
||||
}
|
||||
|
||||
event.makeEncrypted(
|
||||
"m.room.encrypted",
|
||||
|
||||
@@ -24,7 +24,7 @@ import anotherjson from "another-json";
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import OlmDevice from "./OlmDevice";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
@@ -252,13 +252,13 @@ export async function ensureOlmSessionsForDevices(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
if (!olmDevice.sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (v: any) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
delete olmDevice.sessionsInProgress[key];
|
||||
resolve(v);
|
||||
};
|
||||
});
|
||||
@@ -291,9 +291,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key], log,
|
||||
);
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
|
||||
@@ -23,7 +23,7 @@ import { IRoomEncryption } from "../RoomList";
|
||||
import { IDevice } from "../deviceinfo";
|
||||
import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
@@ -125,7 +125,7 @@ export interface CryptoStore {
|
||||
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
|
||||
getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
|
||||
// Session key backups
|
||||
|
||||
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
export const VERSION = 10;
|
||||
|
||||
@@ -34,7 +34,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,18 +25,33 @@ import { EventEmitter } from 'events';
|
||||
import { logger } from '../../logger';
|
||||
import { DeviceInfo } from '../deviceinfo';
|
||||
import { newTimeoutError } from "./Error";
|
||||
import { requestKeysDuringVerification } from "../CrossSigning";
|
||||
import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export class SwitchStartEventError extends Error {
|
||||
constructor(startEvent) {
|
||||
constructor(public readonly startEvent: MatrixEvent) {
|
||||
super();
|
||||
this.startEvent = startEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
private cancelled = false;
|
||||
private _done = false;
|
||||
private promise: Promise<void> = null;
|
||||
private transactionTimeoutTimer: number = null;
|
||||
protected expectedEvent: string;
|
||||
private resolve: () => void;
|
||||
private reject: (e: Error | MatrixEvent) => void;
|
||||
private resolveEvent: (e: MatrixEvent) => void;
|
||||
private rejectEvent: (e: Error) => void;
|
||||
private started: boolean;
|
||||
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
*
|
||||
@@ -64,22 +79,18 @@ export class VerificationBase extends EventEmitter {
|
||||
* @param {object} [request] the key verification request object related to
|
||||
* this verification, if any
|
||||
*/
|
||||
constructor(channel, baseApis, userId, deviceId, startEvent, request) {
|
||||
constructor(
|
||||
public readonly channel: IVerificationChannel,
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly userId: string,
|
||||
public readonly deviceId: string,
|
||||
public startEvent: MatrixEvent,
|
||||
public readonly request: VerificationRequest,
|
||||
) {
|
||||
super();
|
||||
this._channel = channel;
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
|
||||
this.cancelled = false;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
get initiatedByMe() {
|
||||
public get initiatedByMe(): boolean {
|
||||
// if there is no start event yet,
|
||||
// we probably want to send it,
|
||||
// which happens if we initiate
|
||||
@@ -88,16 +99,20 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
const sender = this.startEvent.getSender();
|
||||
const content = this.startEvent.getContent();
|
||||
return sender === this._baseApis.getUserId() &&
|
||||
content.from_device === this._baseApis.getDeviceId();
|
||||
return sender === this.baseApis.getUserId() &&
|
||||
content.from_device === this.baseApis.getDeviceId();
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
public get hasBeenCancelled(): boolean {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
private resetTimer(): void {
|
||||
logger.info("Refreshing/starting the verification transaction timeout timer");
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
if (this.transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this.transactionTimeoutTimer);
|
||||
}
|
||||
this._transactionTimeoutTimer = setTimeout(() => {
|
||||
this.transactionTimeoutTimer = setTimeout(() => {
|
||||
if (!this._done && !this.cancelled) {
|
||||
logger.info("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
@@ -105,18 +120,18 @@ export class VerificationBase extends EventEmitter {
|
||||
}, 10 * 60 * 1000); // 10 minutes
|
||||
}
|
||||
|
||||
_endTimer() {
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
this._transactionTimeoutTimer = null;
|
||||
private endTimer(): void {
|
||||
if (this.transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this.transactionTimeoutTimer);
|
||||
this.transactionTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_send(type, uncompletedContent) {
|
||||
return this._channel.send(type, uncompletedContent);
|
||||
protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
|
||||
return this.channel.send(type, uncompletedContent);
|
||||
}
|
||||
|
||||
_waitForEvent(type) {
|
||||
protected waitForEvent(type: string): Promise<MatrixEvent> {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
@@ -125,24 +140,24 @@ export class VerificationBase extends EventEmitter {
|
||||
return Promise.resolve(existingEvent);
|
||||
}
|
||||
|
||||
this._expectedEvent = type;
|
||||
this.expectedEvent = type;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveEvent = resolve;
|
||||
this._rejectEvent = reject;
|
||||
this.resolveEvent = resolve;
|
||||
this.rejectEvent = reject;
|
||||
});
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
public canSwitchStartEvent(event: MatrixEvent): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
switchStartEvent(event) {
|
||||
public switchStartEvent(event: MatrixEvent): void {
|
||||
if (this.canSwitchStartEvent(event)) {
|
||||
logger.log("Verification Base: switching verification start event",
|
||||
{ restartingFlow: !!this._rejectEvent });
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
{ restartingFlow: !!this.rejectEvent });
|
||||
if (this.rejectEvent) {
|
||||
const reject = this.rejectEvent;
|
||||
this.rejectEvent = undefined;
|
||||
reject(new SwitchStartEventError(event));
|
||||
} else {
|
||||
this.startEvent = event;
|
||||
@@ -150,21 +165,21 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
public handleEvent(e: MatrixEvent): void {
|
||||
if (this._done) {
|
||||
return;
|
||||
} else if (e.getType() === this._expectedEvent) {
|
||||
} else if (e.getType() === this.expectedEvent) {
|
||||
// if we receive an expected m.key.verification.done, then just
|
||||
// ignore it, since we don't need to do anything about it
|
||||
if (this._expectedEvent !== "m.key.verification.done") {
|
||||
this._expectedEvent = undefined;
|
||||
this._rejectEvent = undefined;
|
||||
this._resetTimer();
|
||||
this._resolveEvent(e);
|
||||
if (this.expectedEvent !== "m.key.verification.done") {
|
||||
this.expectedEvent = undefined;
|
||||
this.rejectEvent = undefined;
|
||||
this.resetTimer();
|
||||
this.resolveEvent(e);
|
||||
}
|
||||
} else if (e.getType() === "m.key.verification.cancel") {
|
||||
const reject = this._reject;
|
||||
this._reject = undefined;
|
||||
const reject = this.reject;
|
||||
this.reject = undefined;
|
||||
// there is only promise to reject if verify has been called
|
||||
if (reject) {
|
||||
const content = e.getContent();
|
||||
@@ -172,36 +187,36 @@ export class VerificationBase extends EventEmitter {
|
||||
reject(new Error(`Other side cancelled verification ` +
|
||||
`because ${reason} (${code})`));
|
||||
}
|
||||
} else if (this._expectedEvent) {
|
||||
} else if (this.expectedEvent) {
|
||||
// only cancel if there is an event expected.
|
||||
// if there is no event expected, it means verify() wasn't called
|
||||
// and we're just replaying the timeline events when syncing
|
||||
// after a refresh when the events haven't been stored in the cache yet.
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
"Unexpected message: expecting " + this.expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
);
|
||||
this._expectedEvent = undefined;
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
this.expectedEvent = undefined;
|
||||
if (this.rejectEvent) {
|
||||
const reject = this.rejectEvent;
|
||||
this.rejectEvent = undefined;
|
||||
reject(exception);
|
||||
}
|
||||
this.cancel(exception);
|
||||
}
|
||||
}
|
||||
|
||||
done() {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
public done(): Promise<KeysDuringVerification | void> {
|
||||
this.endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.request.onVerifierFinished();
|
||||
this._resolve();
|
||||
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
|
||||
this.resolve();
|
||||
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
cancel(e) {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
public cancel(e: Error | MatrixEvent): void {
|
||||
this.endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.cancelled = true;
|
||||
this.request.onVerifierCancelled();
|
||||
@@ -210,7 +225,7 @@ export class VerificationBase extends EventEmitter {
|
||||
// cancelled by the other user)
|
||||
if (e === timeoutException) {
|
||||
const timeoutEvent = newTimeoutError();
|
||||
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
this.send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
} else if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
@@ -219,29 +234,29 @@ export class VerificationBase extends EventEmitter {
|
||||
content.code = content.code || "m.unknown";
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
this._send("m.key.verification.cancel", content);
|
||||
this.send("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._send("m.key.verification.cancel", {
|
||||
this.send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._send("m.key.verification.cancel", {
|
||||
this.send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this._promise !== null) {
|
||||
if (this.promise !== null) {
|
||||
// when we cancel without a promise, we end up with a promise
|
||||
// but no reject function. If cancel is called again, we'd error.
|
||||
if (this._reject) this._reject(e);
|
||||
if (this.reject) this.reject(e);
|
||||
} else {
|
||||
// FIXME: this causes an "Uncaught promise" console message
|
||||
// if nothing ends up chaining this promise.
|
||||
this._promise = Promise.reject(e);
|
||||
this.promise = Promise.reject(e);
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
@@ -255,31 +270,32 @@ export class VerificationBase extends EventEmitter {
|
||||
* @returns {Promise} Promise which resolves when the verification has
|
||||
* completed.
|
||||
*/
|
||||
verify() {
|
||||
if (this._promise) return this._promise;
|
||||
public verify(): Promise<void> {
|
||||
if (this.promise) return this.promise;
|
||||
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = (...args) => {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
this.endTimer();
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this.reject = (e: Error) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
reject(...args);
|
||||
this.endTimer();
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
if (this._doVerification && !this._started) {
|
||||
this._started = true;
|
||||
this._resetTimer(); // restart the timeout
|
||||
Promise.resolve(this._doVerification())
|
||||
.then(this.done.bind(this), this.cancel.bind(this));
|
||||
if (this.doVerification && !this.started) {
|
||||
this.started = true;
|
||||
this.resetTimer(); // restart the timeout
|
||||
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
||||
}
|
||||
return this._promise;
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
async _verifyKeys(userId, keys, verifier) {
|
||||
protected doVerification?: () => Promise<void>;
|
||||
|
||||
protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
|
||||
// we try to verify all the keys that we're told about, but we might
|
||||
// not know about all of them, so keep track of the keys that we know
|
||||
// about, and ignore the rest
|
||||
@@ -287,15 +303,14 @@ export class VerificationBase extends EventEmitter {
|
||||
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = this._baseApis.getStoredDevice(userId, deviceId);
|
||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||
if (device) {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
} else {
|
||||
const crossSigningInfo = this._baseApis.crypto.deviceList
|
||||
.getStoredCrossSigningForUser(userId);
|
||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||
await verifier(keyId, DeviceInfo.fromStorage({
|
||||
verifier(keyId, DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
[keyId]: deviceId,
|
||||
},
|
||||
@@ -323,7 +338,11 @@ export class VerificationBase extends EventEmitter {
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
public get events(): string[] | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 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.
|
||||
@@ -22,17 +22,17 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
const content = Object.assign({}, { code, reason }, extradata);
|
||||
export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
|
||||
const content = Object.assign({}, { code, reason }, extraData);
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
export function errorFactory(code, reason) {
|
||||
return function(extradata) {
|
||||
return newVerificationError(code, reason, extradata);
|
||||
export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
|
||||
return function(extraData?: Record<string, any>) {
|
||||
return newVerificationError(code, reason, extraData);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory(
|
||||
"m.invalid_message", "Invalid message",
|
||||
);
|
||||
|
||||
export function errorFromEvent(event) {
|
||||
export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } {
|
||||
const content = event.getContent();
|
||||
if (content) {
|
||||
const { code, reason } = content;
|
||||
+17
-5
@@ -21,23 +21,35 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
static factory(...args) {
|
||||
return new IllegalMethod(...args);
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startEvent: MatrixEvent,
|
||||
request: VerificationRequest,
|
||||
): IllegalMethod {
|
||||
return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
// Typically the name will be something else, but to complete
|
||||
// the contract we offer a default one here.
|
||||
return "org.matrix.illegal_method";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
throw new Error("Verification is not possible with this method");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 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.
|
||||
@@ -21,12 +20,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import {
|
||||
newKeyMismatchError,
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib";
|
||||
import { newKeyMismatchError, newUserCancelledError } from './Error';
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
import { logger } from '../../logger';
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
@@ -36,15 +36,28 @@ export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ReciprocateQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ReciprocateQRCode(...args);
|
||||
public reciprocateQREvent: {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
};
|
||||
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startEvent: MatrixEvent,
|
||||
request: VerificationRequest,
|
||||
): ReciprocateQRCode {
|
||||
return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
return "m.reciprocate.v1";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
if (!this.startEvent) {
|
||||
// TODO: Support scanning QR codes
|
||||
throw new Error("It is not currently possible to start verification" +
|
||||
@@ -58,7 +71,7 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
|
||||
// 2. ask if other user shows shield as well
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.reciprocateQREvent = {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
@@ -67,21 +80,21 @@ export class ReciprocateQRCode extends Base {
|
||||
});
|
||||
|
||||
// 3. determine key to sign / mark as trusted
|
||||
const keys = {};
|
||||
const keys: Record<string, string> = {};
|
||||
|
||||
switch (qrCodeData.mode) {
|
||||
case MODE_VERIFY_OTHER_USER: {
|
||||
case Mode.VerifyOtherUser: {
|
||||
// add master key to keys to be signed, only if we're not doing self-verification
|
||||
const masterKey = qrCodeData.otherUserMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_TRUSTED: {
|
||||
case Mode.VerifySelfTrusted: {
|
||||
const deviceId = this.request.targetDevice.deviceId;
|
||||
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_UNTRUSTED: {
|
||||
case Mode.VerifySelfUntrusted: {
|
||||
const masterKey = qrCodeData.myMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
@@ -89,7 +102,7 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
|
||||
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
|
||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
// make sure the device has the expected keys
|
||||
const targetKey = keys[keyId];
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
@@ -108,103 +121,88 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
enum Mode {
|
||||
VerifyOtherUser = 0x00, // Verifying someone who isn't us
|
||||
VerifySelfTrusted = 0x01, // We trust the master key
|
||||
VerifySelfUntrusted = 0x02, // We do not trust the master key
|
||||
}
|
||||
|
||||
interface IQrData {
|
||||
prefix: string;
|
||||
version: number;
|
||||
mode: Mode;
|
||||
transactionId: string;
|
||||
firstKeyB64: string;
|
||||
secondKeyB64: string;
|
||||
secretB64: string;
|
||||
}
|
||||
|
||||
export class QRCodeData {
|
||||
constructor(
|
||||
mode, sharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey, buffer,
|
||||
) {
|
||||
this._sharedSecret = sharedSecret;
|
||||
this._mode = mode;
|
||||
this._otherUserMasterKey = otherUserMasterKey;
|
||||
this._otherDeviceKey = otherDeviceKey;
|
||||
this._myMasterKey = myMasterKey;
|
||||
this._buffer = buffer;
|
||||
}
|
||||
public readonly mode: Mode,
|
||||
private readonly sharedSecret: string,
|
||||
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
|
||||
public readonly otherUserMasterKey: string | undefined,
|
||||
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
|
||||
public readonly otherDeviceKey: string | undefined,
|
||||
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
|
||||
public readonly myMasterKey: string | undefined,
|
||||
private readonly buffer: Buffer,
|
||||
) {}
|
||||
|
||||
static async create(request, client) {
|
||||
const sharedSecret = QRCodeData._generateSharedSecret();
|
||||
const mode = QRCodeData._determineMode(request, client);
|
||||
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
|
||||
const sharedSecret = QRCodeData.generateSharedSecret();
|
||||
const mode = QRCodeData.determineMode(request, client);
|
||||
let otherUserMasterKey = null;
|
||||
let otherDeviceKey = null;
|
||||
let myMasterKey = null;
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
if (mode === Mode.VerifyOtherUser) {
|
||||
const otherUserCrossSigningInfo =
|
||||
client.getStoredCrossSigningForUser(request.otherUserId);
|
||||
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfTrusted) {
|
||||
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
|
||||
} else if (mode === Mode.VerifySelfUntrusted) {
|
||||
const myUserId = client.getUserId();
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
myMasterKey = myCrossSigningInfo.getId("master");
|
||||
}
|
||||
const qrData = QRCodeData._generateQrData(
|
||||
const qrData = QRCodeData.generateQrData(
|
||||
request, client, mode,
|
||||
sharedSecret,
|
||||
otherUserMasterKey,
|
||||
otherDeviceKey,
|
||||
myMasterKey,
|
||||
);
|
||||
const buffer = QRCodeData._generateBuffer(qrData);
|
||||
const buffer = QRCodeData.generateBuffer(qrData);
|
||||
return new QRCodeData(mode, sharedSecret,
|
||||
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
|
||||
}
|
||||
|
||||
get buffer() {
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_TRUSTED
|
||||
* @return {string} device key of other party at time of generating QR code
|
||||
*/
|
||||
get otherDeviceKey() {
|
||||
return this._otherDeviceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_OTHER_USER
|
||||
* @return {string} master key of other party at time of generating QR code
|
||||
*/
|
||||
get otherUserMasterKey() {
|
||||
return this._otherUserMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
|
||||
* @return {string} own master key at time of generating QR code
|
||||
*/
|
||||
get myMasterKey() {
|
||||
return this._myMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
return this._sharedSecret;
|
||||
public get encodedSharedSecret(): string {
|
||||
return this.sharedSecret;
|
||||
}
|
||||
|
||||
static _generateSharedSecret() {
|
||||
public getBuffer(): Buffer {
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
private static generateSharedSecret(): string {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
static async _getOtherDeviceKey(request, client) {
|
||||
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
|
||||
const myUserId = client.getUserId();
|
||||
const otherDevice = request.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
@@ -212,31 +210,35 @@ export class QRCodeData {
|
||||
if (!device) {
|
||||
throw new Error("could not find device " + otherDeviceId);
|
||||
}
|
||||
const key = device.getFingerprint();
|
||||
return key;
|
||||
return device.getFingerprint();
|
||||
}
|
||||
|
||||
static _determineMode(request, client) {
|
||||
private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
|
||||
const myUserId = client.getUserId();
|
||||
const otherUserId = request.otherUserId;
|
||||
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
let mode = Mode.VerifyOtherUser;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = client.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
mode = Mode.VerifySelfTrusted;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
mode = Mode.VerifySelfUntrusted;
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
static _generateQrData(request, client, mode,
|
||||
encodedSharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey,
|
||||
) {
|
||||
private static generateQrData(
|
||||
request: VerificationRequest,
|
||||
client: MatrixClient,
|
||||
mode: Mode,
|
||||
encodedSharedSecret: string,
|
||||
otherUserMasterKey: string,
|
||||
otherDeviceKey: string,
|
||||
myMasterKey: string,
|
||||
): IQrData {
|
||||
const myUserId = client.getUserId();
|
||||
const transactionId = request.channel.transactionId;
|
||||
const qrData = {
|
||||
@@ -251,16 +253,16 @@ export class QRCodeData {
|
||||
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
if (mode === Mode.VerifyOtherUser) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
// Second key is the other user's master cross signing key
|
||||
qrData.secondKeyB64 = otherUserMasterKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfTrusted) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
qrData.secondKeyB64 = otherDeviceKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfUntrusted) {
|
||||
// First key is our device's key
|
||||
qrData.firstKeyB64 = client.getDeviceEd25519Key();
|
||||
// Second key is what we think our master cross signing key is
|
||||
@@ -269,7 +271,7 @@ export class QRCodeData {
|
||||
return qrData;
|
||||
}
|
||||
|
||||
static _generateBuffer(qrData) {
|
||||
private static generateBuffer(qrData: IQrData): Buffer {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b) => {
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 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.
|
||||
@@ -29,6 +29,8 @@ 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";
|
||||
|
||||
@@ -38,7 +40,7 @@ const EVENTS = [
|
||||
"m.key.verification.mac",
|
||||
];
|
||||
|
||||
let olmutil;
|
||||
let olmutil: Utility;
|
||||
|
||||
const newMismatchedSASError = errorFactory(
|
||||
"m.mismatched_sas", "Mismatched short authentication string",
|
||||
@@ -48,7 +50,7 @@ const newMismatchedCommitmentError = errorFactory(
|
||||
"m.mismatched_commitment", "Mismatched commitment",
|
||||
);
|
||||
|
||||
function generateDecimalSas(sasBytes) {
|
||||
function generateDecimalSas(sasBytes: number[]): [number, number, number] {
|
||||
/**
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|
||||
@@ -64,7 +66,9 @@ function generateDecimalSas(sasBytes) {
|
||||
];
|
||||
}
|
||||
|
||||
const emojiMapping = [
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
|
||||
const emojiMapping: EmojiMapping[] = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
@@ -131,7 +135,7 @@ const emojiMapping = [
|
||||
["📌", "pin"], // 63
|
||||
];
|
||||
|
||||
function generateEmojiSas(sasBytes) {
|
||||
function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
|
||||
const emojis = [
|
||||
// just like base64 encoding
|
||||
sasBytes[0] >> 2,
|
||||
@@ -151,8 +155,20 @@ const sasGenerators = {
|
||||
emoji: generateEmojiSas,
|
||||
};
|
||||
|
||||
function generateSas(sasBytes, methods) {
|
||||
const sas = {};
|
||||
export interface IGeneratedSas {
|
||||
decimal?: [number, number, number];
|
||||
emoji?: EmojiMapping[];
|
||||
}
|
||||
|
||||
export interface ISasEvent {
|
||||
sas: IGeneratedSas;
|
||||
confirm(): Promise<void>;
|
||||
cancel(): void;
|
||||
mismatch(): void;
|
||||
}
|
||||
|
||||
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
|
||||
const sas: IGeneratedSas = {};
|
||||
for (const method of methods) {
|
||||
if (method in sasGenerators) {
|
||||
sas[method] = sasGenerators[method](sasBytes);
|
||||
@@ -166,7 +182,7 @@ const macMethods = {
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
function calculateMAC(olmSAS, method) {
|
||||
function calculateMAC(olmSAS: OlmSAS, method: string) {
|
||||
return function(...args) {
|
||||
const macFunction = olmSAS[macMethods[method]];
|
||||
const mac = macFunction.apply(olmSAS, args);
|
||||
@@ -176,23 +192,23 @@ function calculateMAC(olmSAS, method) {
|
||||
}
|
||||
|
||||
const calculateKeyAgreement = {
|
||||
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
|
||||
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
|
||||
+ `${sas.ourSASPubKey}|`;
|
||||
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS|"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
+ sas.channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
"curve25519": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
|
||||
"curve25519": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||
const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
|
||||
const theirInfo = `${sas.userId}${sas.deviceId}`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
+ sas.channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
};
|
||||
@@ -211,7 +227,7 @@ const HASHES_SET = new Set(HASHES_LIST);
|
||||
const MAC_SET = new Set(MAC_LIST);
|
||||
const SAS_SET = new Set(SAS_LIST);
|
||||
|
||||
function intersection(anArray, aSet) {
|
||||
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
|
||||
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
@@ -220,28 +236,34 @@ function intersection(anArray, aSet) {
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class SAS extends Base {
|
||||
static get NAME() {
|
||||
private waitingForAccept: boolean;
|
||||
public ourSASPubKey: string;
|
||||
public theirSASPubKey: string;
|
||||
public sasEvent: ISasEvent;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
return "m.sas.v1";
|
||||
}
|
||||
|
||||
get events() {
|
||||
public get events(): string[] {
|
||||
return EVENTS;
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
await global.Olm.init();
|
||||
olmutil = olmutil || new global.Olm.Utility();
|
||||
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
await this.baseApis.downloadKeys([this.userId]);
|
||||
|
||||
let retry = false;
|
||||
do {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
return await this._doSendVerification();
|
||||
return await this.doSendVerification();
|
||||
} else {
|
||||
return await this._doRespondVerification();
|
||||
return await this.doRespondVerification();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SwitchStartEventError) {
|
||||
@@ -253,38 +275,37 @@ export class SAS extends Base {
|
||||
}
|
||||
}
|
||||
} while (retry);
|
||||
}
|
||||
};
|
||||
|
||||
canSwitchStartEvent(event) {
|
||||
public canSwitchStartEvent(event: MatrixEvent): boolean {
|
||||
if (event.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
return content && content.method === SAS.NAME &&
|
||||
this._waitingForAccept;
|
||||
return content && content.method === SAS.NAME && this.waitingForAccept;
|
||||
}
|
||||
|
||||
async _sendStart() {
|
||||
const startContent = this._channel.completeContent(START_TYPE, {
|
||||
private async sendStart(): Promise<Record<string, any>> {
|
||||
const startContent = this.channel.completeContent(START_TYPE, {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
from_device: this.baseApis.deviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
hashes: HASHES_LIST,
|
||||
message_authentication_codes: MAC_LIST,
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: SAS_LIST,
|
||||
});
|
||||
await this._channel.sendCompleted(START_TYPE, startContent);
|
||||
await this.channel.sendCompleted(START_TYPE, startContent);
|
||||
return startContent;
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
this._waitingForAccept = true;
|
||||
private async doSendVerification(): Promise<void> {
|
||||
this.waitingForAccept = true;
|
||||
let startContent;
|
||||
if (this.startEvent) {
|
||||
startContent = this._channel.completedContentFromEvent(this.startEvent);
|
||||
startContent = this.channel.completedContentFromEvent(this.startEvent);
|
||||
} else {
|
||||
startContent = await this._sendStart();
|
||||
startContent = await this.sendStart();
|
||||
}
|
||||
|
||||
// we might have switched to a different start event,
|
||||
@@ -297,9 +318,9 @@ export class SAS extends Base {
|
||||
|
||||
let e;
|
||||
try {
|
||||
e = await this._waitForEvent("m.key.verification.accept");
|
||||
e = await this.waitForEvent("m.key.verification.accept");
|
||||
} finally {
|
||||
this._waitingForAccept = false;
|
||||
this.waitingForAccept = false;
|
||||
}
|
||||
let content = e.getContent();
|
||||
const sasMethods
|
||||
@@ -319,11 +340,11 @@ export class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
await this.send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
e = await this.waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(startContent);
|
||||
@@ -335,12 +356,12 @@ export class SAS extends Base {
|
||||
olmSAS.set_their_key(content.key);
|
||||
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
const verifySAS = new Promise<void>((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
await this.sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
@@ -353,54 +374,45 @@ export class SAS extends Base {
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac")
|
||||
this.waitForEvent("m.key.verification.mac")
|
||||
.then((e) => {
|
||||
// we don't expect any more messages from the other
|
||||
// party, and they may send a m.key.verification.done
|
||||
// when they're done on their end
|
||||
this._expectedEvent = "m.key.verification.done";
|
||||
this.expectedEvent = "m.key.verification.done";
|
||||
return e;
|
||||
}),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
await this.checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
async _doRespondVerification() {
|
||||
private async doRespondVerification(): Promise<void> {
|
||||
// as m.related_to is not included in the encrypted content in e2e rooms,
|
||||
// we need to make sure it is added
|
||||
let content = this._channel.completedContentFromEvent(this.startEvent);
|
||||
let content = this.channel.completedContentFromEvent(this.startEvent);
|
||||
|
||||
// Note: we intersect using our pre-made lists, rather than the sets,
|
||||
// so that the result will be in our order of preference. Then
|
||||
// fetching the first element from the array will give our preferred
|
||||
// method out of the ones offered by the other party.
|
||||
const keyAgreement
|
||||
= intersection(
|
||||
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
|
||||
)[0];
|
||||
const hashMethod
|
||||
= intersection(HASHES_LIST, new Set(content.hashes))[0];
|
||||
const macMethod
|
||||
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
|
||||
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
|
||||
const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
|
||||
const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
const sasMethods
|
||||
= intersection(content.short_authentication_string, SAS_SET);
|
||||
if (!(keyAgreement !== undefined
|
||||
&& hashMethod !== undefined
|
||||
&& macMethod !== undefined
|
||||
&& sasMethods.length)) {
|
||||
const sasMethods = intersection(content.short_authentication_string, SAS_SET);
|
||||
if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
await this._send("m.key.verification.accept", {
|
||||
await this.send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -409,23 +421,23 @@ export class SAS extends Base {
|
||||
commitment: olmutil.sha256(commitmentStr),
|
||||
});
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.key");
|
||||
let e = await this.waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
this.theirSASPubKey = content.key;
|
||||
olmSAS.set_their_key(content.key);
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
await this.send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
const verifySAS = new Promise<void>((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
await this.sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
@@ -438,39 +450,39 @@ export class SAS extends Base {
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac")
|
||||
this.waitForEvent("m.key.verification.mac")
|
||||
.then((e) => {
|
||||
// we don't expect any more messages from the other
|
||||
// party, and they may send a m.key.verification.done
|
||||
// when they're done on their end
|
||||
this._expectedEvent = "m.key.verification.done";
|
||||
this.expectedEvent = "m.key.verification.done";
|
||||
return e;
|
||||
}),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
await this.checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS, method) {
|
||||
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
|
||||
const mac = {};
|
||||
const keyList = [];
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.baseApis.getUserId() + this.baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this._channel.transactionId;
|
||||
+ this.channel.transactionId;
|
||||
|
||||
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
|
||||
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
this.baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + deviceKeyId,
|
||||
);
|
||||
keyList.push(deviceKeyId);
|
||||
|
||||
const crossSigningId = this._baseApis.getCrossSigningId();
|
||||
const crossSigningId = this.baseApis.getCrossSigningId();
|
||||
if (crossSigningId) {
|
||||
const crossSigningKeyId = `ed25519:${crossSigningId}`;
|
||||
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
|
||||
@@ -484,14 +496,14 @@ export class SAS extends Base {
|
||||
keyList.sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
return this._send("m.key.verification.mac", { mac, keys });
|
||||
return this.send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this._channel.transactionId;
|
||||
+ this.baseApis.getUserId() + this.baseApis.deviceId
|
||||
+ this.channel.transactionId;
|
||||
|
||||
if (content.keys !== calculateMAC(olmSAS, method)(
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
@@ -500,7 +512,7 @@ export class SAS extends Base {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== calculateMAC(olmSAS, method)(
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationRequest } from "./VerificationRequest";
|
||||
|
||||
export interface IVerificationChannel {
|
||||
request?: VerificationRequest;
|
||||
readonly userId: string;
|
||||
readonly roomId?: string;
|
||||
readonly deviceId?: string;
|
||||
readonly transactionId: string;
|
||||
readonly receiveStartFromOtherDevices?: boolean;
|
||||
getTimestamp(event: MatrixEvent): number;
|
||||
send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
|
||||
completeContent(type: string, content: Record<string, any>): Record<string, any>;
|
||||
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
|
||||
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
|
||||
canCreateRequest(type: string): boolean;
|
||||
}
|
||||
+66
-65
@@ -22,8 +22,12 @@ import {
|
||||
START_TYPE,
|
||||
} from "./VerificationRequest";
|
||||
import { logger } from '../../../logger';
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { EventType } from "../../../@types/event";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
|
||||
const MESSAGE_TYPE = "m.room.message";
|
||||
const MESSAGE_TYPE = EventType.RoomMessage;
|
||||
const M_REFERENCE = "m.reference";
|
||||
const M_RELATES_TO = "m.relates_to";
|
||||
|
||||
@@ -31,36 +35,34 @@ const M_RELATES_TO = "m.relates_to";
|
||||
* A key verification channel that sends verification events in the timeline of a room.
|
||||
* Uses the event id of the initial m.key.verification.request event as a transaction id.
|
||||
*/
|
||||
export class InRoomChannel {
|
||||
export class InRoomChannel implements IVerificationChannel {
|
||||
private requestEventId = null;
|
||||
|
||||
/**
|
||||
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
|
||||
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
||||
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
||||
*/
|
||||
constructor(client, roomId, userId = null) {
|
||||
this._client = client;
|
||||
this._roomId = roomId;
|
||||
this.userId = userId;
|
||||
this._requestEventId = null;
|
||||
constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly roomId: string,
|
||||
public userId: string = null,
|
||||
) {
|
||||
}
|
||||
|
||||
get receiveStartFromOtherDevices() {
|
||||
public get receiveStartFromOtherDevices(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get roomId() {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
/** The transaction id generated/used by this verification channel */
|
||||
get transactionId() {
|
||||
return this._requestEventId;
|
||||
public get transactionId(): string {
|
||||
return this.requestEventId;
|
||||
}
|
||||
|
||||
static getOtherPartyUserId(event, client) {
|
||||
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
if (type !== REQUEST_TYPE) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const ownUserId = client.getUserId();
|
||||
const sender = event.getSender();
|
||||
@@ -78,25 +80,29 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
getTimestamp(event) {
|
||||
public getTimestamp(event: MatrixEvent): number {
|
||||
return event.getTs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
|
||||
* @param {string} type the event type to check
|
||||
* @returns {bool} boolean flag
|
||||
* @returns {boolean} boolean flag
|
||||
*/
|
||||
static canCreateRequest(type) {
|
||||
public static canCreateRequest(type: string): boolean {
|
||||
return type === REQUEST_TYPE;
|
||||
}
|
||||
|
||||
public canCreateRequest(type: string): boolean {
|
||||
return InRoomChannel.canCreateRequest(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the transaction id used by a given key verification event, if any
|
||||
* @param {MatrixEvent} event the event
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static getTransactionId(event) {
|
||||
public static getTransactionId(event: MatrixEvent): string {
|
||||
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
|
||||
return event.getId();
|
||||
} else {
|
||||
@@ -114,9 +120,9 @@ export class InRoomChannel {
|
||||
* `handleEvent` can do more checks and choose to ignore invalid events.
|
||||
* @param {MatrixEvent} event the event to validate
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(event, client) {
|
||||
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
|
||||
const txnId = InRoomChannel.getTransactionId(event);
|
||||
if (typeof txnId !== "string" || txnId.length === 0) {
|
||||
return false;
|
||||
@@ -152,7 +158,7 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the event to get the type of
|
||||
* @returns {string} the "symbolic" event type
|
||||
*/
|
||||
static getEventType(event) {
|
||||
public static getEventType(event: MatrixEvent): string {
|
||||
const type = event.getType();
|
||||
if (type === MESSAGE_TYPE) {
|
||||
const content = event.getContent();
|
||||
@@ -174,10 +180,10 @@ export class InRoomChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
|
||||
// prevent processing the same event multiple times, as under
|
||||
// some circumstances Room.timeline can get emitted twice for the same event
|
||||
if (request.hasEventId(event.getId())) {
|
||||
@@ -187,18 +193,18 @@ export class InRoomChannel {
|
||||
// do validations that need state (roomId, userId),
|
||||
// ignore if invalid
|
||||
|
||||
if (event.getRoomId() !== this._roomId) {
|
||||
if (event.getRoomId() !== this.roomId) {
|
||||
return;
|
||||
}
|
||||
// set userId if not set already
|
||||
if (this.userId === null) {
|
||||
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
|
||||
const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
|
||||
if (userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
// ignore events not sent by us or the other party
|
||||
const ownUserId = this._client.getUserId();
|
||||
const ownUserId = this.client.getUserId();
|
||||
const sender = event.getSender();
|
||||
if (this.userId !== null) {
|
||||
if (sender !== ownUserId && sender !== this.userId) {
|
||||
@@ -207,12 +213,12 @@ export class InRoomChannel {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this._requestEventId === null) {
|
||||
this._requestEventId = InRoomChannel.getTransactionId(event);
|
||||
if (this.requestEventId === null) {
|
||||
this.requestEventId = InRoomChannel.getTransactionId(event);
|
||||
}
|
||||
|
||||
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
||||
const isSentByUs = event.getSender() === this._client.getUserId();
|
||||
const isSentByUs = event.getSender() === this.client.getUserId();
|
||||
|
||||
return await request.handleEvent(
|
||||
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
||||
@@ -226,13 +232,14 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the received event
|
||||
* @returns {Object} the content object with the relation added again
|
||||
*/
|
||||
completedContentFromEvent(event) {
|
||||
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
|
||||
// ensure m.related_to is included in e2ee rooms
|
||||
// as the field is excluded from encryption
|
||||
const content = Object.assign({}, event.getContent());
|
||||
content[M_RELATES_TO] = event.getRelation();
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all the fields to content needed for sending it over this channel.
|
||||
* This is public so verification methods (SAS uses this) can get the exact
|
||||
@@ -242,15 +249,15 @@ export class InRoomChannel {
|
||||
* @param {object} content the (incomplete) content
|
||||
* @returns {object} the complete content, as it will be sent.
|
||||
*/
|
||||
completeContent(type, content) {
|
||||
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
|
||||
content = Object.assign({}, content);
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
content.from_device = this.client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
// type is mapped to m.room.message in the send method
|
||||
content = {
|
||||
body: this._client.getUserId() + " is requesting to verify " +
|
||||
body: this.client.getUserId() + " is requesting to verify " +
|
||||
"your key, but your client does not support in-chat key " +
|
||||
"verification. You will need to use legacy key " +
|
||||
"verification to verify keys.",
|
||||
@@ -274,7 +281,7 @@ export class InRoomChannel {
|
||||
* @param {object} uncompletedContent the (incomplete) content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
send(type, uncompletedContent) {
|
||||
public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
|
||||
const content = this.completeContent(type, uncompletedContent);
|
||||
return this.sendCompleted(type, content);
|
||||
}
|
||||
@@ -285,74 +292,68 @@ export class InRoomChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
async sendCompleted(type, content) {
|
||||
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
|
||||
let sendType = type;
|
||||
if (type === REQUEST_TYPE) {
|
||||
sendType = MESSAGE_TYPE;
|
||||
}
|
||||
const response = await this._client.sendEvent(this._roomId, sendType, content);
|
||||
const response = await this.client.sendEvent(this.roomId, sendType, content);
|
||||
if (type === REQUEST_TYPE) {
|
||||
this._requestEventId = response.event_id;
|
||||
this.requestEventId = response.event_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InRoomRequests {
|
||||
constructor() {
|
||||
this._requestsByRoomId = new Map();
|
||||
}
|
||||
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
|
||||
|
||||
getRequest(event) {
|
||||
public getRequest(event: MatrixEvent): VerificationRequest {
|
||||
const roomId = event.getRoomId();
|
||||
const txnId = InRoomChannel.getTransactionId(event);
|
||||
return this._getRequestByTxnId(roomId, txnId);
|
||||
return this.getRequestByTxnId(roomId, txnId);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||
public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
|
||||
return this.getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||
}
|
||||
|
||||
_getRequestByTxnId(roomId, txnId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this._setRequest(
|
||||
event.getRoomId(),
|
||||
InRoomChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
public setRequest(event: MatrixEvent, request: VerificationRequest): void {
|
||||
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
this._setRequest(channel.roomId, channel.transactionId, request);
|
||||
public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
|
||||
this.doSetRequest(channel.roomId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
_setRequest(roomId, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
|
||||
let requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByRoomId.set(roomId, requestsByTxnId);
|
||||
this.requestsByRoomId.set(roomId, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
public removeRequest(event: MatrixEvent): void {
|
||||
const roomId = event.getRoomId();
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByRoomId.delete(roomId);
|
||||
this.requestsByRoomId.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(roomId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
public findRequestInProgress(roomId: string): VerificationRequest {
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending) {
|
||||
+74
-78
@@ -28,26 +28,31 @@ import {
|
||||
} from "./VerificationRequest";
|
||||
import { errorFromEvent, newUnexpectedMessageError } from "../Error";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { MatrixClient } from "../../../client";
|
||||
|
||||
type Request = VerificationRequest<ToDeviceChannel>;
|
||||
|
||||
/**
|
||||
* A key verification channel that sends verification events over to_device messages.
|
||||
* Generates its own transaction ids.
|
||||
*/
|
||||
export class ToDeviceChannel {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
||||
this._client = client;
|
||||
this.userId = userId;
|
||||
this._devices = devices;
|
||||
this.transactionId = transactionId;
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
export class ToDeviceChannel implements IVerificationChannel {
|
||||
public request?: VerificationRequest;
|
||||
|
||||
isToDevices(devices) {
|
||||
if (devices.length === this._devices.length) {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly userId: string,
|
||||
private readonly devices: string[],
|
||||
public transactionId: string = null,
|
||||
public deviceId: string = null,
|
||||
) {}
|
||||
|
||||
public isToDevices(devices: string[]): boolean {
|
||||
if (devices.length === this.devices.length) {
|
||||
for (const device of devices) {
|
||||
const d = this._devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!d) {
|
||||
if (!this.devices.includes(device)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,11 +62,7 @@ export class ToDeviceChannel {
|
||||
}
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this._deviceId;
|
||||
}
|
||||
|
||||
static getEventType(event) {
|
||||
public static getEventType(event: MatrixEvent): string {
|
||||
return event.getType();
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the event
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static getTransactionId(event) {
|
||||
public static getTransactionId(event: MatrixEvent): string {
|
||||
const content = event.getContent();
|
||||
return content && content.transaction_id;
|
||||
}
|
||||
@@ -78,12 +79,16 @@ export class ToDeviceChannel {
|
||||
/**
|
||||
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
|
||||
* @param {string} type the event type to check
|
||||
* @returns {bool} boolean flag
|
||||
* @returns {boolean} boolean flag
|
||||
*/
|
||||
static canCreateRequest(type) {
|
||||
public static canCreateRequest(type: string): boolean {
|
||||
return type === REQUEST_TYPE || type === START_TYPE;
|
||||
}
|
||||
|
||||
public canCreateRequest(type: string): boolean {
|
||||
return ToDeviceChannel.canCreateRequest(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this event is a well-formed key verification event.
|
||||
* This only does checks that don't rely on the current state of a potentially already channel
|
||||
@@ -91,9 +96,9 @@ export class ToDeviceChannel {
|
||||
* `handleEvent` can do more checks and choose to ignore invalid events.
|
||||
* @param {MatrixEvent} event the event to validate
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(event, client) {
|
||||
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
|
||||
if (event.isCancelled()) {
|
||||
logger.warn("Ignoring flagged verification request from "
|
||||
+ event.getSender());
|
||||
@@ -134,7 +139,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
getTimestamp(event) {
|
||||
public getTimestamp(event: MatrixEvent): number {
|
||||
const content = event.getContent();
|
||||
return content && content.timestamp;
|
||||
}
|
||||
@@ -143,10 +148,10 @@ export class ToDeviceChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
|
||||
const type = event.getType();
|
||||
const content = event.getContent();
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
@@ -155,17 +160,16 @@ export class ToDeviceChannel {
|
||||
}
|
||||
const deviceId = content.from_device;
|
||||
// adopt deviceId if not set before and valid
|
||||
if (!this._deviceId && this._devices.includes(deviceId)) {
|
||||
this._deviceId = deviceId;
|
||||
if (!this.deviceId && this.devices.includes(deviceId)) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
// if no device id or different from addopted one, cancel with sender
|
||||
if (!this._deviceId || this._deviceId !== deviceId) {
|
||||
// if no device id or different from adopted one, cancel with sender
|
||||
if (!this.deviceId || this.deviceId !== deviceId) {
|
||||
// also check that message came from the device we sent the request to earlier on
|
||||
// and do send a cancel message to that device
|
||||
// (but don't cancel the request for the device we should be talking to)
|
||||
const cancelContent =
|
||||
this.completeContent(errorFromEvent(newUnexpectedMessageError()));
|
||||
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||
const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
|
||||
return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||
}
|
||||
}
|
||||
const wasStarted = request.phase === PHASE_STARTED ||
|
||||
@@ -178,16 +182,16 @@ export class ToDeviceChannel {
|
||||
|
||||
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||
// the request has picked a ready or start event, tell the other devices about it
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||
const nonChosenDevices = this._devices.filter(
|
||||
d => d !== this._deviceId && d !== this._client.getDeviceId(),
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
|
||||
const nonChosenDevices = this.devices.filter(
|
||||
d => d !== this.deviceId && d !== this.client.getDeviceId(),
|
||||
);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
const message = this.completeContent(CANCEL_TYPE, {
|
||||
code: "m.accepted",
|
||||
reason: "Verification request accepted by another device",
|
||||
});
|
||||
await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
|
||||
await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +201,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the received event
|
||||
* @returns {Object} the content object
|
||||
*/
|
||||
completedContentFromEvent(event) {
|
||||
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
|
||||
return event.getContent();
|
||||
}
|
||||
|
||||
@@ -210,14 +214,14 @@ export class ToDeviceChannel {
|
||||
* @param {object} content the (incomplete) content
|
||||
* @returns {object} the complete content, as it will be sent.
|
||||
*/
|
||||
completeContent(type, content) {
|
||||
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
|
||||
// make a copy
|
||||
content = Object.assign({}, content);
|
||||
if (this.transactionId) {
|
||||
content.transaction_id = this.transactionId;
|
||||
}
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
content.from_device = this.client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
content.timestamp = Date.now();
|
||||
@@ -231,7 +235,7 @@ export class ToDeviceChannel {
|
||||
* @param {object} uncompletedContent the (incomplete) content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
send(type, uncompletedContent = {}) {
|
||||
public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
|
||||
// create transaction id when sending request
|
||||
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
|
||||
this.transactionId = ToDeviceChannel.makeTransactionId();
|
||||
@@ -246,21 +250,21 @@ export class ToDeviceChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
async sendCompleted(type, content) {
|
||||
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
|
||||
let result;
|
||||
if (type === REQUEST_TYPE) {
|
||||
result = await this._sendToDevices(type, content, this._devices);
|
||||
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
|
||||
result = await this.sendToDevices(type, content, this.devices);
|
||||
} else {
|
||||
result = await this._sendToDevices(type, content, [this._deviceId]);
|
||||
result = await this.sendToDevices(type, content, [this.deviceId]);
|
||||
}
|
||||
// the VerificationRequest state machine requires remote echos of the event
|
||||
// the client sends itself, so we fake this for to_device messages
|
||||
const remoteEchoEvent = new MatrixEvent({
|
||||
sender: this._client.getUserId(),
|
||||
sender: this.client.getUserId(),
|
||||
content,
|
||||
type,
|
||||
});
|
||||
await this._request.handleEvent(
|
||||
await this.request.handleEvent(
|
||||
type,
|
||||
remoteEchoEvent,
|
||||
/*isLiveEvent=*/true,
|
||||
@@ -270,16 +274,14 @@ export class ToDeviceChannel {
|
||||
return result;
|
||||
}
|
||||
|
||||
_sendToDevices(type, content, devices) {
|
||||
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
|
||||
if (devices.length) {
|
||||
const msgMap = {};
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = content;
|
||||
}
|
||||
|
||||
return this._client.sendToDevice(type, { [this.userId]: msgMap });
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
await this.client.sendToDevice(type, { [this.userId]: msgMap });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,68 +289,62 @@ export class ToDeviceChannel {
|
||||
* Allow Crypto module to create and know the transaction id before the .start event gets sent.
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static makeTransactionId() {
|
||||
public static makeTransactionId(): string {
|
||||
return randomString(32);
|
||||
}
|
||||
}
|
||||
|
||||
export class ToDeviceRequests {
|
||||
constructor() {
|
||||
this._requestsByUserId = new Map();
|
||||
}
|
||||
private requestsByUserId = new Map<string, Map<string, Request>>();
|
||||
|
||||
getRequest(event) {
|
||||
public getRequest(event: MatrixEvent): Request {
|
||||
return this.getRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
public getRequestByChannel(channel: ToDeviceChannel): Request {
|
||||
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
|
||||
}
|
||||
|
||||
getRequestBySenderAndTxnId(sender, txnId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
|
||||
const requestsByTxnId = this.requestsByUserId.get(sender);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this.setRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
public setRequest(event: MatrixEvent, request: Request): void {
|
||||
this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
|
||||
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
setRequestBySenderAndTxnId(sender, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
|
||||
let requestsByTxnId = this.requestsByUserId.get(sender);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByUserId.set(sender, requestsByTxnId);
|
||||
this.requestsByUserId.set(sender, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
public removeRequest(event: MatrixEvent): void {
|
||||
const userId = event.getSender();
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByUserId.delete(userId);
|
||||
this.requestsByUserId.delete(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(userId, devices) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
public findRequestInProgress(userId: string, devices: string[]): Request {
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending && request.channel.isToDevices(devices)) {
|
||||
@@ -358,8 +354,8 @@ export class ToDeviceRequests {
|
||||
}
|
||||
}
|
||||
|
||||
getRequestsInProgress(userId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
public getRequestsInProgress(userId: string): Request[] {
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
|
||||
}
|
||||
+222
-193
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 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.
|
||||
@@ -24,6 +23,11 @@ import {
|
||||
newUnknownMethodError,
|
||||
} from "../Error";
|
||||
import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationBase } from "../Base";
|
||||
import { VerificationMethod } from "../../index";
|
||||
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
@@ -44,12 +48,32 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
||||
export const DONE_TYPE = EVENT_PREFIX + "done";
|
||||
export const READY_TYPE = EVENT_PREFIX + "ready";
|
||||
|
||||
export const PHASE_UNSENT = 1;
|
||||
export const PHASE_REQUESTED = 2;
|
||||
export const PHASE_READY = 3;
|
||||
export const PHASE_STARTED = 4;
|
||||
export const PHASE_CANCELLED = 5;
|
||||
export const PHASE_DONE = 6;
|
||||
export enum Phase {
|
||||
Unsent = 1,
|
||||
Requested,
|
||||
Ready,
|
||||
Started,
|
||||
Cancelled,
|
||||
Done,
|
||||
}
|
||||
|
||||
// Legacy export fields
|
||||
export const PHASE_UNSENT = Phase.Unsent;
|
||||
export const PHASE_REQUESTED = Phase.Requested;
|
||||
export const PHASE_READY = Phase.Ready;
|
||||
export const PHASE_STARTED = Phase.Started;
|
||||
export const PHASE_CANCELLED = Phase.Cancelled;
|
||||
export const PHASE_DONE = Phase.Done;
|
||||
|
||||
interface ITargetDevice {
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
}
|
||||
|
||||
interface ITransition {
|
||||
phase: Phase;
|
||||
event?: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* State machine for verification requests.
|
||||
@@ -57,32 +81,38 @@ export const PHASE_DONE = 6;
|
||||
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
|
||||
* @event "change" whenever the state of the request object has changed.
|
||||
*/
|
||||
export class VerificationRequest extends EventEmitter {
|
||||
constructor(channel, verificationMethods, client) {
|
||||
super();
|
||||
this.channel = channel;
|
||||
this.channel._request = this;
|
||||
this._verificationMethods = verificationMethods;
|
||||
this._client = client;
|
||||
this._commonMethods = [];
|
||||
this._setPhase(PHASE_UNSENT, false);
|
||||
this._eventsByUs = new Map();
|
||||
this._eventsByThem = new Map();
|
||||
this._observeOnly = false;
|
||||
this._timeoutTimer = null;
|
||||
this._accepting = false;
|
||||
this._declining = false;
|
||||
this._verifierHasFinished = false;
|
||||
this._cancelled = false;
|
||||
this._chosenMethod = null;
|
||||
// we keep a copy of the QR Code data (including other user master key) around
|
||||
// for QR reciprocate verification, to protect against
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
this._qrCodeData = null;
|
||||
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
|
||||
private eventsByUs = new Map<string, MatrixEvent>();
|
||||
private eventsByThem = new Map<string, MatrixEvent>();
|
||||
private _observeOnly = false;
|
||||
private timeoutTimer: number = null;
|
||||
private _accepting = false;
|
||||
private _declining = false;
|
||||
private verifierHasFinished = false;
|
||||
private _cancelled = false;
|
||||
private _chosenMethod: VerificationMethod = null;
|
||||
// we keep a copy of the QR Code data (including other user master key) around
|
||||
// for QR reciprocate verification, to protect against
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
private _qrCodeData: QRCodeData = null;
|
||||
|
||||
// The timestamp when we received the request event from the other side
|
||||
this._requestReceivedAt = null;
|
||||
// The timestamp when we received the request event from the other side
|
||||
private requestReceivedAt: number = null;
|
||||
|
||||
private commonMethods: VerificationMethod[] = [];
|
||||
private _phase: Phase;
|
||||
private _cancellingUserId: string;
|
||||
private _verifier: VerificationBase;
|
||||
|
||||
constructor(
|
||||
public readonly channel: C,
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
|
||||
private readonly client: MatrixClient,
|
||||
) {
|
||||
super();
|
||||
this.channel.request = this;
|
||||
this.setPhase(PHASE_UNSENT, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,9 +121,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(type, event, client) {
|
||||
public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
|
||||
const content = event.getContent();
|
||||
|
||||
if (!type || !type.startsWith(EVENT_PREFIX)) {
|
||||
@@ -128,53 +158,53 @@ export class VerificationRequest extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
get invalid() {
|
||||
public get invalid(): boolean {
|
||||
return this.phase === PHASE_UNSENT;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_REQUESTED */
|
||||
get requested() {
|
||||
public get requested(): boolean {
|
||||
return this.phase === PHASE_REQUESTED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_CANCELLED */
|
||||
get cancelled() {
|
||||
public get cancelled(): boolean {
|
||||
return this.phase === PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_READY */
|
||||
get ready() {
|
||||
public get ready(): boolean {
|
||||
return this.phase === PHASE_READY;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_STARTED */
|
||||
get started() {
|
||||
public get started(): boolean {
|
||||
return this.phase === PHASE_STARTED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_DONE */
|
||||
get done() {
|
||||
public get done(): boolean {
|
||||
return this.phase === PHASE_DONE;
|
||||
}
|
||||
|
||||
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
|
||||
get methods() {
|
||||
return this._commonMethods;
|
||||
public get methods(): VerificationMethod[] {
|
||||
return this.commonMethods;
|
||||
}
|
||||
|
||||
/** the method picked in the .start event */
|
||||
get chosenMethod() {
|
||||
public get chosenMethod(): VerificationMethod {
|
||||
return this._chosenMethod;
|
||||
}
|
||||
|
||||
calculateEventTimeout(event) {
|
||||
public calculateEventTimeout(event: MatrixEvent): number {
|
||||
let effectiveExpiresAt = this.channel.getTimestamp(event)
|
||||
+ TIMEOUT_FROM_EVENT_TS;
|
||||
|
||||
if (this._requestReceivedAt && !this.initiatedByMe &&
|
||||
if (this.requestReceivedAt && !this.initiatedByMe &&
|
||||
this.phase <= PHASE_REQUESTED
|
||||
) {
|
||||
const expiresAtByReceipt = this._requestReceivedAt
|
||||
const expiresAtByReceipt = this.requestReceivedAt
|
||||
+ TIMEOUT_FROM_EVENT_RECEIPT;
|
||||
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
|
||||
}
|
||||
@@ -183,8 +213,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
/** The current remaining amount of ms before the request should be automatically cancelled */
|
||||
get timeout() {
|
||||
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||
public get timeout(): number {
|
||||
const requestEvent = this.getEventByEither(REQUEST_TYPE);
|
||||
if (requestEvent) {
|
||||
return this.calculateEventTimeout(requestEvent);
|
||||
}
|
||||
@@ -195,41 +225,41 @@ export class VerificationRequest extends EventEmitter {
|
||||
* The key verification request event.
|
||||
* @returns {MatrixEvent} The request event, or falsey if not found.
|
||||
*/
|
||||
get requestEvent() {
|
||||
return this._getEventByEither(REQUEST_TYPE);
|
||||
public get requestEvent(): MatrixEvent {
|
||||
return this.getEventByEither(REQUEST_TYPE);
|
||||
}
|
||||
|
||||
/** current phase of the request. Some properties might only be defined in a current phase. */
|
||||
get phase() {
|
||||
public get phase(): Phase {
|
||||
return this._phase;
|
||||
}
|
||||
|
||||
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
|
||||
get verifier() {
|
||||
public get verifier(): VerificationBase {
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
get canAccept() {
|
||||
public get canAccept(): boolean {
|
||||
return this.phase < PHASE_READY && !this._accepting && !this._declining;
|
||||
}
|
||||
|
||||
get accepting() {
|
||||
public get accepting(): boolean {
|
||||
return this._accepting;
|
||||
}
|
||||
|
||||
get declining() {
|
||||
public get declining(): boolean {
|
||||
return this._declining;
|
||||
}
|
||||
|
||||
/** whether this request has sent it's initial event and needs more events to complete */
|
||||
get pending() {
|
||||
public get pending(): boolean {
|
||||
return !this.observeOnly &&
|
||||
this._phase !== PHASE_DONE &&
|
||||
this._phase !== PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** Only set after a .ready if the other party can scan a QR code */
|
||||
get qrCodeData() {
|
||||
public get qrCodeData(): QRCodeData {
|
||||
return this._qrCodeData;
|
||||
}
|
||||
|
||||
@@ -239,19 +269,19 @@ export class VerificationRequest extends EventEmitter {
|
||||
* For methods that need to be supported by both ends, use the `methods` property.
|
||||
* @param {string} method the method to check
|
||||
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
|
||||
* @return {bool} whether or not the other party said the supported the method */
|
||||
otherPartySupportsMethod(method, force = false) {
|
||||
* @return {boolean} whether or not the other party said the supported the method */
|
||||
public otherPartySupportsMethod(method: string, force = false): boolean {
|
||||
if (!force && !this.ready && !this.started) {
|
||||
return false;
|
||||
}
|
||||
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
|
||||
this._eventsByThem.get(READY_TYPE);
|
||||
const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) ||
|
||||
this.eventsByThem.get(READY_TYPE);
|
||||
if (!theirMethodEvent) {
|
||||
// if we started straight away with .start event,
|
||||
// we are assuming that the other side will support the
|
||||
// chosen method, so return true for that.
|
||||
if (this.started && this.initiatedByMe) {
|
||||
const myStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
const myStartEvent = this.eventsByUs.get(START_TYPE);
|
||||
const content = myStartEvent && myStartEvent.getContent();
|
||||
const myStartMethod = content && content.method;
|
||||
return method == myStartMethod;
|
||||
@@ -274,22 +304,22 @@ export class VerificationRequest extends EventEmitter {
|
||||
* For InRoomChannel, this is who sent the .request event.
|
||||
* For ToDeviceChannel, this is who sent the .start event
|
||||
*/
|
||||
get initiatedByMe() {
|
||||
public get initiatedByMe(): boolean {
|
||||
// event created by us but no remote echo has been received yet
|
||||
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
|
||||
const noEventsYet = (this.eventsByUs.size + this.eventsByThem.size) === 0;
|
||||
if (this._phase === PHASE_UNSENT && noEventsYet) {
|
||||
return true;
|
||||
}
|
||||
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
|
||||
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
|
||||
const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
|
||||
const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
|
||||
if (hasMyRequest && !hasTheirRequest) {
|
||||
return true;
|
||||
}
|
||||
if (!hasMyRequest && hasTheirRequest) {
|
||||
return false;
|
||||
}
|
||||
const hasMyStart = this._eventsByUs.has(START_TYPE);
|
||||
const hasTheirStart = this._eventsByThem.has(START_TYPE);
|
||||
const hasMyStart = this.eventsByUs.has(START_TYPE);
|
||||
const hasTheirStart = this.eventsByThem.has(START_TYPE);
|
||||
if (hasMyStart && !hasTheirStart) {
|
||||
return true;
|
||||
}
|
||||
@@ -297,39 +327,39 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
/** The id of the user that initiated the request */
|
||||
get requestingUserId() {
|
||||
public get requestingUserId(): string {
|
||||
if (this.initiatedByMe) {
|
||||
return this._client.getUserId();
|
||||
return this.client.getUserId();
|
||||
} else {
|
||||
return this.otherUserId;
|
||||
}
|
||||
}
|
||||
|
||||
/** The id of the user that (will) receive(d) the request */
|
||||
get receivingUserId() {
|
||||
public get receivingUserId(): string {
|
||||
if (this.initiatedByMe) {
|
||||
return this.otherUserId;
|
||||
} else {
|
||||
return this._client.getUserId();
|
||||
return this.client.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
/** The user id of the other party in this request */
|
||||
get otherUserId() {
|
||||
public get otherUserId(): string {
|
||||
return this.channel.userId;
|
||||
}
|
||||
|
||||
get isSelfVerification() {
|
||||
return this._client.getUserId() === this.otherUserId;
|
||||
public get isSelfVerification(): boolean {
|
||||
return this.client.getUserId() === this.otherUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the user that cancelled the request,
|
||||
* only defined when phase is PHASE_CANCELLED
|
||||
*/
|
||||
get cancellingUserId() {
|
||||
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
|
||||
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
|
||||
public get cancellingUserId(): string {
|
||||
const myCancel = this.eventsByUs.get(CANCEL_TYPE);
|
||||
const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
|
||||
|
||||
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
|
||||
return myCancel.getSender();
|
||||
@@ -343,12 +373,12 @@ export class VerificationRequest extends EventEmitter {
|
||||
/**
|
||||
* The cancellation code e.g m.user which is responsible for cancelling this verification
|
||||
*/
|
||||
get cancellationCode() {
|
||||
const ev = this._getEventByEither(CANCEL_TYPE);
|
||||
public get cancellationCode(): string {
|
||||
const ev = this.getEventByEither(CANCEL_TYPE);
|
||||
return ev ? ev.getContent().code : null;
|
||||
}
|
||||
|
||||
get observeOnly() {
|
||||
public get observeOnly(): boolean {
|
||||
return this._observeOnly;
|
||||
}
|
||||
|
||||
@@ -359,11 +389,11 @@ export class VerificationRequest extends EventEmitter {
|
||||
* verification to when no specific device is specified.
|
||||
* @returns {{userId: *, deviceId: *}} The device information
|
||||
*/
|
||||
get targetDevice() {
|
||||
public get targetDevice(): ITargetDevice {
|
||||
const theirFirstEvent =
|
||||
this._eventsByThem.get(REQUEST_TYPE) ||
|
||||
this._eventsByThem.get(READY_TYPE) ||
|
||||
this._eventsByThem.get(START_TYPE);
|
||||
this.eventsByThem.get(REQUEST_TYPE) ||
|
||||
this.eventsByThem.get(READY_TYPE) ||
|
||||
this.eventsByThem.get(START_TYPE);
|
||||
const theirFirstContent = theirFirstEvent.getContent();
|
||||
const fromDevice = theirFirstContent.from_device;
|
||||
return {
|
||||
@@ -379,21 +409,20 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
|
||||
* @returns {VerifierBase} the verifier of the given method
|
||||
*/
|
||||
beginKeyVerification(method, targetDevice = null) {
|
||||
public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase {
|
||||
// need to allow also when unsent in case of to_device
|
||||
if (!this.observeOnly && !this._verifier) {
|
||||
const validStartPhase =
|
||||
this.phase === PHASE_REQUESTED ||
|
||||
this.phase === PHASE_READY ||
|
||||
(this.phase === PHASE_UNSENT &&
|
||||
this.channel.constructor.canCreateRequest(START_TYPE));
|
||||
(this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
|
||||
if (validStartPhase) {
|
||||
// when called on a request that was initiated with .request event
|
||||
// check the method is supported by both sides
|
||||
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
|
||||
if (this.commonMethods.length && !this.commonMethods.includes(method)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
this._verifier = this._createVerifier(method, null, targetDevice);
|
||||
this._verifier = this.createVerifier(method, null, targetDevice);
|
||||
if (!this._verifier) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
@@ -407,9 +436,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
* sends the initial .request event.
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async sendRequest() {
|
||||
public async sendRequest(): Promise<void> {
|
||||
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
await this.channel.send(REQUEST_TYPE, { methods });
|
||||
}
|
||||
}
|
||||
@@ -420,14 +449,14 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string?} error.code the error code to send the cancellation with
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async cancel({ reason = "User declined", code = "m.user" } = {}) {
|
||||
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
|
||||
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||
this._declining = true;
|
||||
this.emit("change");
|
||||
if (this._verifier) {
|
||||
return this._verifier.cancel(errorFactory(code, reason)());
|
||||
} else {
|
||||
this._cancellingUserId = this._client.getUserId();
|
||||
this._cancellingUserId = this.client.getUserId();
|
||||
await this.channel.send(CANCEL_TYPE, { code, reason });
|
||||
}
|
||||
}
|
||||
@@ -437,9 +466,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
* Accepts the request, sending a .ready event to the other party
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async accept() {
|
||||
public async accept(): Promise<void> {
|
||||
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
this._accepting = true;
|
||||
this.emit("change");
|
||||
await this.channel.send(READY_TYPE, { methods });
|
||||
@@ -453,7 +482,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @returns {Promise} that resolves once the callback returns true
|
||||
* @throws {Error} when the request is cancelled
|
||||
*/
|
||||
waitFor(fn) {
|
||||
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = () => {
|
||||
let handled = false;
|
||||
@@ -475,46 +504,46 @@ export class VerificationRequest extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
_setPhase(phase, notify = true) {
|
||||
private setPhase(phase: Phase, notify = true): void {
|
||||
this._phase = phase;
|
||||
if (notify) {
|
||||
this.emit("change");
|
||||
}
|
||||
}
|
||||
|
||||
_getEventByEither(type) {
|
||||
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
|
||||
private getEventByEither(type: string): MatrixEvent {
|
||||
return this.eventsByThem.get(type) || this.eventsByUs.get(type);
|
||||
}
|
||||
|
||||
_getEventBy(type, byThem) {
|
||||
private getEventBy(type: string, byThem = false): MatrixEvent {
|
||||
if (byThem) {
|
||||
return this._eventsByThem.get(type);
|
||||
return this.eventsByThem.get(type);
|
||||
} else {
|
||||
return this._eventsByUs.get(type);
|
||||
return this.eventsByUs.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
_calculatePhaseTransitions() {
|
||||
const transitions = [{ phase: PHASE_UNSENT }];
|
||||
private calculatePhaseTransitions(): ITransition[] {
|
||||
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
|
||||
const phase = () => transitions[transitions.length - 1].phase;
|
||||
|
||||
// always pass by .request first to be sure channel.userId has been set
|
||||
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
|
||||
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
|
||||
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
|
||||
const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
|
||||
if (requestEvent) {
|
||||
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
|
||||
}
|
||||
|
||||
const readyEvent =
|
||||
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
|
||||
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
|
||||
if (readyEvent && phase() === PHASE_REQUESTED) {
|
||||
transitions.push({ phase: PHASE_READY, event: readyEvent });
|
||||
}
|
||||
|
||||
let startEvent;
|
||||
if (readyEvent || !requestEvent) {
|
||||
const theirStartEvent = this._eventsByThem.get(START_TYPE);
|
||||
const ourStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
const theirStartEvent = this.eventsByThem.get(START_TYPE);
|
||||
const ourStartEvent = this.eventsByUs.get(START_TYPE);
|
||||
// any party can send .start after a .ready or unsent
|
||||
if (theirStartEvent && ourStartEvent) {
|
||||
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
|
||||
@@ -523,24 +552,22 @@ export class VerificationRequest extends EventEmitter {
|
||||
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
|
||||
}
|
||||
} else {
|
||||
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
|
||||
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
|
||||
}
|
||||
if (startEvent) {
|
||||
const fromRequestPhase = phase() === PHASE_REQUESTED &&
|
||||
requestEvent.getSender() !== startEvent.getSender();
|
||||
const fromUnsentPhase = phase() === PHASE_UNSENT &&
|
||||
this.channel.constructor.canCreateRequest(START_TYPE);
|
||||
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
|
||||
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
|
||||
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
|
||||
transitions.push({ phase: PHASE_STARTED, event: startEvent });
|
||||
}
|
||||
}
|
||||
|
||||
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
|
||||
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
|
||||
const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
|
||||
if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
|
||||
transitions.push({ phase: PHASE_DONE });
|
||||
}
|
||||
|
||||
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
||||
const cancelEvent = this.getEventByEither(CANCEL_TYPE);
|
||||
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
|
||||
transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
|
||||
return transitions;
|
||||
@@ -549,14 +576,14 @@ export class VerificationRequest extends EventEmitter {
|
||||
return transitions;
|
||||
}
|
||||
|
||||
_transitionToPhase(transition) {
|
||||
private transitionToPhase(transition: ITransition): void {
|
||||
const { phase, event } = transition;
|
||||
// get common methods
|
||||
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
|
||||
if (!this._wasSentByOwnDevice(event)) {
|
||||
if (!this.wasSentByOwnDevice(event)) {
|
||||
const content = event.getContent();
|
||||
this._commonMethods =
|
||||
content.methods.filter(m => this._verificationMethods.has(m));
|
||||
this.commonMethods =
|
||||
content.methods.filter(m => this.verificationMethods.has(m));
|
||||
}
|
||||
}
|
||||
// detect if we're not a party in the request, and we should just observe
|
||||
@@ -568,8 +595,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
) {
|
||||
if (
|
||||
this.channel.receiveStartFromOtherDevices &&
|
||||
this._wasSentByOwnUser(event) &&
|
||||
!this._wasSentByOwnDevice(event)
|
||||
this.wasSentByOwnUser(event) &&
|
||||
!this.wasSentByOwnDevice(event)
|
||||
) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
@@ -579,7 +606,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (phase === PHASE_STARTED) {
|
||||
const { method } = event.getContent();
|
||||
if (!this._verifier && !this.observeOnly) {
|
||||
this._verifier = this._createVerifier(method, event);
|
||||
this._verifier = this.createVerifier(method, event);
|
||||
if (!this._verifier) {
|
||||
this.cancel({
|
||||
code: "m.unknown_method",
|
||||
@@ -592,19 +619,19 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_applyPhaseTransitions() {
|
||||
const transitions = this._calculatePhaseTransitions();
|
||||
private applyPhaseTransitions(): ITransition[] {
|
||||
const transitions = this.calculatePhaseTransitions();
|
||||
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
|
||||
// trim off phases we already went through, if any
|
||||
const newTransitions = transitions.slice(existingIdx + 1);
|
||||
// transition to all new phases
|
||||
for (const transition of newTransitions) {
|
||||
this._transitionToPhase(transition);
|
||||
this.transitionToPhase(transition);
|
||||
}
|
||||
return newTransitions;
|
||||
}
|
||||
|
||||
_isWinningStartRace(newEvent) {
|
||||
private isWinningStartRace(newEvent: MatrixEvent): boolean {
|
||||
if (newEvent.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
@@ -620,13 +647,13 @@ export class VerificationRequest extends EventEmitter {
|
||||
const oldContent = oldEvent.getContent();
|
||||
oldRaceIdentifier = oldContent && oldContent.from_device;
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getDeviceId();
|
||||
oldRaceIdentifier = this.client.getDeviceId();
|
||||
}
|
||||
} else {
|
||||
if (oldEvent) {
|
||||
oldRaceIdentifier = oldEvent.getSender();
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getUserId();
|
||||
oldRaceIdentifier = this.client.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,13 +667,13 @@ export class VerificationRequest extends EventEmitter {
|
||||
return newRaceIdentifier < oldRaceIdentifier;
|
||||
}
|
||||
|
||||
hasEventId(eventId) {
|
||||
for (const event of this._eventsByUs.values()) {
|
||||
public hasEventId(eventId: string): boolean {
|
||||
for (const event of this.eventsByUs.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const event of this._eventsByThem.values()) {
|
||||
for (const event of this.eventsByThem.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
@@ -658,23 +685,29 @@ export class VerificationRequest extends EventEmitter {
|
||||
* Changes the state of the request and verifier in response to a key verification event.
|
||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
|
||||
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device
|
||||
* @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
|
||||
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
|
||||
public async handleEvent(
|
||||
type: string,
|
||||
event: MatrixEvent,
|
||||
isLiveEvent: boolean,
|
||||
isRemoteEcho: boolean,
|
||||
isSentByUs: boolean,
|
||||
): Promise<void> {
|
||||
// if reached phase cancelled or done, ignore anything else that comes
|
||||
if (this.done || this.cancelled) {
|
||||
return;
|
||||
}
|
||||
const wasObserveOnly = this._observeOnly;
|
||||
|
||||
this._adjustObserveOnly(event, isLiveEvent);
|
||||
this.adjustObserveOnly(event, isLiveEvent);
|
||||
|
||||
if (!this.observeOnly && !isRemoteEcho) {
|
||||
if (await this._cancelOnError(type, event)) {
|
||||
if (await this.cancelOnError(type, event)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -685,27 +718,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
// added here to prevent verification getting cancelled
|
||||
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
|
||||
const isDuplicateEvent = isSentByUs ?
|
||||
this._eventsByUs.has(type) :
|
||||
this._eventsByThem.has(type);
|
||||
this.eventsByUs.has(type) :
|
||||
this.eventsByThem.has(type);
|
||||
if (isDuplicateEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPhase = this.phase;
|
||||
this._addEvent(type, event, isSentByUs);
|
||||
this.addEvent(type, event, isSentByUs);
|
||||
|
||||
// this will create if needed the verifier so needs to happen before calling it
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
try {
|
||||
// only pass events from the other side to the verifier,
|
||||
// no remote echos of our own events
|
||||
if (this._verifier && !this.observeOnly) {
|
||||
const newEventWinsRace = this._isWinningStartRace(event);
|
||||
const newEventWinsRace = this.isWinningStartRace(event);
|
||||
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
|
||||
this._verifier.switchStartEvent(event);
|
||||
} else if (!isRemoteEcho) {
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
&& this._verifier.events.includes(type))) {
|
||||
if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
|
||||
this._verifier.handleEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -722,16 +754,16 @@ export class VerificationRequest extends EventEmitter {
|
||||
const shouldGenerateQrCode =
|
||||
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
|
||||
if (shouldGenerateQrCode) {
|
||||
this._qrCodeData = await QRCodeData.create(this, this._client);
|
||||
this._qrCodeData = await QRCodeData.create(this, this.client);
|
||||
}
|
||||
}
|
||||
|
||||
const lastTransition = newTransitions[newTransitions.length - 1];
|
||||
const { phase } = lastTransition;
|
||||
|
||||
this._setupTimeout(phase);
|
||||
this.setupTimeout(phase);
|
||||
// set phase as last thing as this emits the "change" event
|
||||
this._setPhase(phase);
|
||||
this.setPhase(phase);
|
||||
} else if (this._observeOnly !== wasObserveOnly) {
|
||||
this.emit("change");
|
||||
}
|
||||
@@ -748,26 +780,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
private setupTimeout(phase: Phase): void {
|
||||
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
|
||||
}
|
||||
if (this._timeoutTimer) {
|
||||
if (this.timeoutTimer) {
|
||||
const shouldClear = phase === PHASE_STARTED ||
|
||||
phase === PHASE_READY ||
|
||||
phase === PHASE_DONE ||
|
||||
phase === PHASE_CANCELLED;
|
||||
if (shouldClear) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
clearTimeout(this.timeoutTimer);
|
||||
this.timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cancelOnTimeout = () => {
|
||||
private cancelOnTimeout = () => {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
@@ -785,10 +817,10 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
async _cancelOnError(type, event) {
|
||||
private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
|
||||
if (type === START_TYPE) {
|
||||
const method = event.getContent().method;
|
||||
if (!this._verificationMethods.has(method)) {
|
||||
if (!this.verificationMethods.has(method)) {
|
||||
await this.cancel(errorFromEvent(newUnknownMethodError()));
|
||||
return true;
|
||||
}
|
||||
@@ -811,7 +843,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
_adjustObserveOnly(event, isLiveEvent) {
|
||||
private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
|
||||
// don't send out events for historical requests
|
||||
if (!isLiveEvent) {
|
||||
this._observeOnly = true;
|
||||
@@ -821,83 +853,80 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_addEvent(type, event, isSentByUs) {
|
||||
private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
|
||||
if (isSentByUs) {
|
||||
this._eventsByUs.set(type, event);
|
||||
this.eventsByUs.set(type, event);
|
||||
} else {
|
||||
this._eventsByThem.set(type, event);
|
||||
this.eventsByThem.set(type, event);
|
||||
}
|
||||
|
||||
// once we know the userId of the other party (from the .request event)
|
||||
// see if any event by anyone else crept into this._eventsByThem
|
||||
// see if any event by anyone else crept into this.eventsByThem
|
||||
if (type === REQUEST_TYPE) {
|
||||
for (const [type, event] of this._eventsByThem.entries()) {
|
||||
for (const [type, event] of this.eventsByThem.entries()) {
|
||||
if (event.getSender() !== this.otherUserId) {
|
||||
this._eventsByThem.delete(type);
|
||||
this.eventsByThem.delete(type);
|
||||
}
|
||||
}
|
||||
// also remember when we received the request event
|
||||
this._requestReceivedAt = Date.now();
|
||||
this.requestReceivedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
_createVerifier(method, startEvent = null, targetDevice = null) {
|
||||
private createVerifier(
|
||||
method: VerificationMethod,
|
||||
startEvent: MatrixEvent = null,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase {
|
||||
if (!targetDevice) {
|
||||
targetDevice = this.targetDevice;
|
||||
}
|
||||
const { userId, deviceId } = targetDevice;
|
||||
|
||||
const VerifierCtor = this._verificationMethods.get(method);
|
||||
const VerifierCtor = this.verificationMethods.get(method);
|
||||
if (!VerifierCtor) {
|
||||
logger.warn("could not find verifier constructor for method", method);
|
||||
return;
|
||||
}
|
||||
return new VerifierCtor(
|
||||
this.channel,
|
||||
this._client,
|
||||
userId,
|
||||
deviceId,
|
||||
startEvent,
|
||||
this,
|
||||
);
|
||||
return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
|
||||
}
|
||||
|
||||
_wasSentByOwnUser(event) {
|
||||
return event.getSender() === this._client.getUserId();
|
||||
private wasSentByOwnUser(event: MatrixEvent): boolean {
|
||||
return event.getSender() === this.client.getUserId();
|
||||
}
|
||||
|
||||
// only for .request, .ready or .start
|
||||
_wasSentByOwnDevice(event) {
|
||||
if (!this._wasSentByOwnUser(event)) {
|
||||
private wasSentByOwnDevice(event: MatrixEvent): boolean {
|
||||
if (!this.wasSentByOwnUser(event)) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
if (!content || content.from_device !== this._client.getDeviceId()) {
|
||||
if (!content || content.from_device !== this.client.getDeviceId()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onVerifierCancelled() {
|
||||
public onVerifierCancelled(): void {
|
||||
this._cancelled = true;
|
||||
// move to cancelled phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
this.setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
onVerifierFinished() {
|
||||
public onVerifierFinished(): void {
|
||||
this.channel.send("m.key.verification.done", {});
|
||||
this._verifierHasFinished = true;
|
||||
this.verifierHasFinished = true;
|
||||
// move to .done phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
this.setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
getEventFromOtherParty(type) {
|
||||
return this._eventsByThem.get(type);
|
||||
public getEventFromOtherParty(type: string): MatrixEvent {
|
||||
return this.eventsByThem.get(type);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,90 @@ limitations under the License.
|
||||
|
||||
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";
|
||||
|
||||
interface IFlow {
|
||||
stages: AuthType[];
|
||||
}
|
||||
|
||||
export interface IInputs {
|
||||
emailAddress?: string;
|
||||
phoneCountry?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
export interface IStageStatus {
|
||||
emailSid?: string;
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IAuthData {
|
||||
session?: string;
|
||||
completed?: string[];
|
||||
flows?: IFlow[];
|
||||
params?: Record<string, Record<string, any>>;
|
||||
errcode?: string;
|
||||
error?: MatrixError;
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
Password = "m.login.password",
|
||||
Recaptcha = "m.login.recaptcha",
|
||||
Terms = "m.login.terms",
|
||||
Email = "m.login.email.identity",
|
||||
Msisdn = "m.login.msisdn",
|
||||
Sso = "m.login.sso",
|
||||
SsoUnstable = "org.matrix.login.sso",
|
||||
Dummy = "m.login.dummy",
|
||||
}
|
||||
|
||||
export interface IAuthDict {
|
||||
// [key: string]: any;
|
||||
type?: string;
|
||||
// session?: string; // TODO
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
identifier?: any;
|
||||
password?: string;
|
||||
response?: string;
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
// eslint-disable-next-line camelcase
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
}
|
||||
|
||||
class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
|
||||
super(m);
|
||||
}
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
matrixClient: MatrixClient;
|
||||
authData?: IAuthData;
|
||||
inputs?: IInputs;
|
||||
sessionId?: string;
|
||||
clientSecret?: string;
|
||||
emailSid?: string;
|
||||
doRequest(auth: IAuthData, background: boolean): Promise<IAuthData>;
|
||||
stateUpdated(nextStage: AuthType, status: IStageStatus): void;
|
||||
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
|
||||
busyChanged?(busy: boolean): void;
|
||||
startAuthStage?(nextStage: string): Promise<void>; // LEGACY
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts the logic used to drive the interactive auth process.
|
||||
*
|
||||
@@ -50,12 +130,12 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
* called with the new auth dict to submit the request. Also passes a
|
||||
* second deprecated arg which is a flag set to true if this request
|
||||
* is a background request. The busyChanged callback should be used
|
||||
* instead of the backfround flag. Should return a promise which resolves
|
||||
* instead of the background flag. Should return a promise which resolves
|
||||
* to the successful response or rejects with a MatrixError.
|
||||
*
|
||||
* @param {function(bool): Promise} opts.busyChanged
|
||||
* @param {function(boolean): Promise} opts.busyChanged
|
||||
* called whenever the interactive auth logic becomes busy submitting
|
||||
* information provided by the user or finsihes. After this has been
|
||||
* information provided by the user or finishes. After this has been
|
||||
* called with true the UI should indicate that a request is in progress
|
||||
* until it is called again with false.
|
||||
*
|
||||
@@ -101,33 +181,41 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
* attemptAuth promise.
|
||||
*
|
||||
*/
|
||||
export function InteractiveAuth(opts) {
|
||||
this._matrixClient = opts.matrixClient;
|
||||
this._data = opts.authData || {};
|
||||
this._requestCallback = opts.doRequest;
|
||||
this._busyChangedCallback = opts.busyChanged;
|
||||
// startAuthStage included for backwards compat
|
||||
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
this._inputs = opts.inputs || {};
|
||||
this._requestEmailTokenCallback = opts.requestEmailToken;
|
||||
export class InteractiveAuth {
|
||||
private readonly matrixClient: MatrixClient;
|
||||
private readonly inputs: IInputs;
|
||||
private readonly clientSecret: string;
|
||||
private readonly requestCallback: IOpts["doRequest"];
|
||||
private readonly busyChangedCallback?: IOpts["busyChanged"];
|
||||
private readonly stateUpdatedCallback: IOpts["stateUpdated"];
|
||||
private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
|
||||
|
||||
if (opts.sessionId) this._data.session = opts.sessionId;
|
||||
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
|
||||
this._emailSid = opts.emailSid;
|
||||
if (this._emailSid === undefined) this._emailSid = null;
|
||||
this._requestingEmailToken = false;
|
||||
|
||||
this._chosenFlow = null;
|
||||
this._currentStage = null;
|
||||
private data: IAuthData;
|
||||
private emailSid?: string;
|
||||
private requestingEmailToken = false;
|
||||
private attemptAuthDeferred: IDeferred<IAuthData> = null;
|
||||
private chosenFlow: IFlow = null;
|
||||
private currentStage: string = null;
|
||||
|
||||
// if we are currently trying to submit an auth dict (which includes polling)
|
||||
// the promise the will resolve/reject when it completes
|
||||
this._submitPromise = null;
|
||||
}
|
||||
private submitPromise: Promise<void> = null;
|
||||
|
||||
constructor(opts: IOpts) {
|
||||
this.matrixClient = opts.matrixClient;
|
||||
this.data = opts.authData || {};
|
||||
this.requestCallback = opts.doRequest;
|
||||
this.busyChangedCallback = opts.busyChanged;
|
||||
// startAuthStage included for backwards compat
|
||||
this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
|
||||
this.requestEmailTokenCallback = opts.requestEmailToken;
|
||||
this.inputs = opts.inputs || {};
|
||||
|
||||
if (opts.sessionId) this.data.session = opts.sessionId;
|
||||
this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret();
|
||||
this.emailSid = opts.emailSid ?? null;
|
||||
}
|
||||
|
||||
InteractiveAuth.prototype = {
|
||||
/**
|
||||
* begin the authentication process.
|
||||
*
|
||||
@@ -135,58 +223,57 @@ InteractiveAuth.prototype = {
|
||||
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
|
||||
* no suitable authentication flow can be found
|
||||
*/
|
||||
attemptAuth: function() {
|
||||
public attemptAuth(): Promise<IAuthData> {
|
||||
// This promise will be quite long-lived and will resolve when the
|
||||
// request is authenticated and completes successfully.
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveFunc = resolve;
|
||||
this._rejectFunc = reject;
|
||||
this.attemptAuthDeferred = defer();
|
||||
// pluck the promise out now, as doRequest may clear before we return
|
||||
const promise = this.attemptAuthDeferred.promise;
|
||||
|
||||
const hasFlows = this._data && this._data.flows;
|
||||
|
||||
// if we have no flows, try a request to acquire the flows
|
||||
if (!hasFlows) {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(true);
|
||||
// use the existing sessionid, if one is present.
|
||||
let auth = null;
|
||||
if (this._data.session) {
|
||||
auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
}
|
||||
this._doRequest(auth).finally(() => {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(false);
|
||||
});
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
// if we have no flows, try a request to acquire the flows
|
||||
if (!this.data?.flows) {
|
||||
this.busyChangedCallback?.(true);
|
||||
// use the existing sessionId, if one is present.
|
||||
let auth = null;
|
||||
if (this.data.session) {
|
||||
auth = {
|
||||
session: this.data.session,
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
this.doRequest(auth).finally(() => {
|
||||
this.busyChangedCallback?.(false);
|
||||
});
|
||||
} else {
|
||||
this.startNextAuthStage();
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll to check if the auth session or current stage has been
|
||||
* completed out-of-band. If so, the attemptAuth promise will
|
||||
* be resolved.
|
||||
*/
|
||||
poll: async function() {
|
||||
if (!this._data.session) return;
|
||||
public async poll(): Promise<void> {
|
||||
if (!this.data.session) return;
|
||||
// likewise don't poll if there is no auth session in progress
|
||||
if (!this._resolveFunc) return;
|
||||
if (!this.attemptAuthDeferred) return;
|
||||
// if we currently have a request in flight, there's no point making
|
||||
// another just to check what the status is
|
||||
if (this._submitPromise) return;
|
||||
if (this.submitPromise) return;
|
||||
|
||||
let authDict = {};
|
||||
if (this._currentStage == EMAIL_STAGE_TYPE) {
|
||||
let authDict: IAuthDict = {};
|
||||
if (this.currentStage == EMAIL_STAGE_TYPE) {
|
||||
// The email can be validated out-of-band, but we need to provide the
|
||||
// creds so the HS can go & check it.
|
||||
if (this._emailSid) {
|
||||
const creds = {
|
||||
sid: this._emailSid,
|
||||
client_secret: this._clientSecret,
|
||||
if (this.emailSid) {
|
||||
const creds: Record<string, string> = {
|
||||
sid: this.emailSid,
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
if (await this._matrixClient.doesServerRequireIdServerParam()) {
|
||||
const idServerParsedUrl = new URL(this._matrixClient.getIdentityServerUrl());
|
||||
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
||||
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
authDict = {
|
||||
@@ -201,16 +288,16 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
|
||||
this.submitAuthDict(authDict, true);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* get the auth session ID
|
||||
*
|
||||
* @return {string} session id
|
||||
*/
|
||||
getSessionId: function() {
|
||||
return this._data ? this._data.session : undefined;
|
||||
},
|
||||
public getSessionId(): string {
|
||||
return this.data ? this.data.session : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the client secret used for validation sessions
|
||||
@@ -218,9 +305,9 @@ InteractiveAuth.prototype = {
|
||||
*
|
||||
* @return {string} client secret
|
||||
*/
|
||||
getClientSecret: function() {
|
||||
return this._clientSecret;
|
||||
},
|
||||
public getClientSecret(): string {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the server params for a given stage
|
||||
@@ -228,17 +315,13 @@ InteractiveAuth.prototype = {
|
||||
* @param {string} loginType login type for the stage
|
||||
* @return {object?} any parameters from the server for this stage
|
||||
*/
|
||||
getStageParams: function(loginType) {
|
||||
let params = {};
|
||||
if (this._data && this._data.params) {
|
||||
params = this._data.params;
|
||||
}
|
||||
return params[loginType];
|
||||
},
|
||||
public getStageParams(loginType: string): Record<string, any> {
|
||||
return this.data.params?.[loginType];
|
||||
}
|
||||
|
||||
getChosenFlow() {
|
||||
return this._chosenFlow;
|
||||
},
|
||||
public getChosenFlow(): IFlow {
|
||||
return this.chosenFlow;
|
||||
}
|
||||
|
||||
/**
|
||||
* submit a new auth dict and fire off the request. This will either
|
||||
@@ -246,38 +329,38 @@ InteractiveAuth.prototype = {
|
||||
* to be called for a new stage.
|
||||
*
|
||||
* @param {object} authData new auth dict to send to the server. Should
|
||||
* include a `type` propterty denoting the login type, as well as any
|
||||
* include a `type` property denoting the login type, as well as any
|
||||
* other params for that stage.
|
||||
* @param {bool} background If true, this request failing will not result
|
||||
* @param {boolean} background If true, this request failing will not result
|
||||
* in the attemptAuth promise being rejected. This can be set to true
|
||||
* for requests that just poll to see if auth has been completed elsewhere.
|
||||
*/
|
||||
submitAuthDict: async function(authData, background) {
|
||||
if (!this._resolveFunc) {
|
||||
public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
|
||||
if (!this.attemptAuthDeferred) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
|
||||
if (!background && this._busyChangedCallback) {
|
||||
this._busyChangedCallback(true);
|
||||
if (!background) {
|
||||
this.busyChangedCallback?.(true);
|
||||
}
|
||||
|
||||
// if we're currently trying a request, wait for it to finish
|
||||
// as otherwise we can get multiple 200 responses which can mean
|
||||
// things like multiple logins for register requests.
|
||||
// (but discard any expections as we only care when its done,
|
||||
// (but discard any exceptions as we only care when its done,
|
||||
// not whether it worked or not)
|
||||
while (this._submitPromise) {
|
||||
while (this.submitPromise) {
|
||||
try {
|
||||
await this._submitPromise;
|
||||
await this.submitPromise;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth;
|
||||
if (this._data.session) {
|
||||
if (this.data.session) {
|
||||
auth = {
|
||||
session: this._data.session,
|
||||
session: this.data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
} else {
|
||||
@@ -287,15 +370,15 @@ InteractiveAuth.prototype = {
|
||||
try {
|
||||
// NB. the 'background' flag is deprecated by the busyChanged
|
||||
// callback and is here for backwards compat
|
||||
this._submitPromise = this._doRequest(auth, background);
|
||||
await this._submitPromise;
|
||||
this.submitPromise = this.doRequest(auth, background);
|
||||
await this.submitPromise;
|
||||
} finally {
|
||||
this._submitPromise = null;
|
||||
if (!background && this._busyChangedCallback) {
|
||||
this._busyChangedCallback(false);
|
||||
this.submitPromise = null;
|
||||
if (!background) {
|
||||
this.busyChangedCallback?.(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sid for the email validation session
|
||||
@@ -303,9 +386,9 @@ InteractiveAuth.prototype = {
|
||||
*
|
||||
* @returns {string} The sid of the email auth session
|
||||
*/
|
||||
getEmailSid: function() {
|
||||
return this._emailSid;
|
||||
},
|
||||
public getEmailSid(): string {
|
||||
return this.emailSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sid for the email validation session
|
||||
@@ -315,9 +398,9 @@ InteractiveAuth.prototype = {
|
||||
*
|
||||
* @param {string} sid The sid for the email validation session
|
||||
*/
|
||||
setEmailSid: function(sid) {
|
||||
this._emailSid = sid;
|
||||
},
|
||||
public setEmailSid(sid: string): void {
|
||||
this.emailSid = sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire off a request, and either resolve the promise, or call
|
||||
@@ -325,33 +408,29 @@ InteractiveAuth.prototype = {
|
||||
*
|
||||
* @private
|
||||
* @param {object?} auth new auth dict, including session id
|
||||
* @param {bool?} background If true, this request is a background poll, so it
|
||||
* @param {boolean?} background If true, this request is a background poll, so it
|
||||
* failing will not result in the attemptAuth promise being rejected.
|
||||
* This can be set to true for requests that just poll to see if auth has
|
||||
* been completed elsewhere.
|
||||
*/
|
||||
_doRequest: async function(auth, background) {
|
||||
private async doRequest(auth: IAuthData, background = false): Promise<void> {
|
||||
try {
|
||||
const result = await this._requestCallback(auth, background);
|
||||
this._resolveFunc(result);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
const result = await this.requestCallback(auth, background);
|
||||
this.attemptAuthDeferred.resolve(result);
|
||||
this.attemptAuthDeferred = null;
|
||||
} catch (error) {
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = error.data ? error.data.flows : null;
|
||||
const haveFlows = this._data.flows || Boolean(errorFlows);
|
||||
const errorFlows = error.data?.flows ?? null;
|
||||
const haveFlows = this.data.flows || Boolean(errorFlows);
|
||||
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
this._rejectFunc(error);
|
||||
this.attemptAuthDeferred?.reject(error);
|
||||
} else {
|
||||
// We ignore all failures here (even non-UI auth related ones)
|
||||
// since we don't want to suddenly fail if the internet connection
|
||||
// had a blip whilst we were polling
|
||||
logger.log(
|
||||
"Background poll request failed doing UI auth: ignoring",
|
||||
error,
|
||||
);
|
||||
logger.log("Background poll request failed doing UI auth: ignoring", error);
|
||||
}
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
@@ -360,37 +439,36 @@ InteractiveAuth.prototype = {
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (!error.data.flows && !error.data.completed && !error.data.session) {
|
||||
error.data.flows = this._data.flows;
|
||||
error.data.completed = this._data.completed;
|
||||
error.data.session = this._data.session;
|
||||
error.data.flows = this.data.flows;
|
||||
error.data.completed = this.data.completed;
|
||||
error.data.session = this.data.session;
|
||||
}
|
||||
this._data = error.data;
|
||||
this.data = error.data;
|
||||
try {
|
||||
this._startNextAuthStage();
|
||||
this.startNextAuthStage();
|
||||
} catch (e) {
|
||||
this._rejectFunc(e);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
this.attemptAuthDeferred.reject(e);
|
||||
this.attemptAuthDeferred = null;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._emailSid &&
|
||||
!this._requestingEmailToken &&
|
||||
this._chosenFlow.stages.includes('m.login.email.identity')
|
||||
!this.emailSid &&
|
||||
!this.requestingEmailToken &&
|
||||
this.chosenFlow.stages.includes(AuthType.Email)
|
||||
) {
|
||||
// If we've picked a flow with email auth, we send the email
|
||||
// now because we want the request to fail as soon as possible
|
||||
// if the email address is not valid (ie. already taken or not
|
||||
// registered, depending on what the operation is).
|
||||
this._requestingEmailToken = true;
|
||||
this.requestingEmailToken = true;
|
||||
try {
|
||||
const requestTokenResult = await this._requestEmailTokenCallback(
|
||||
this._inputs.emailAddress,
|
||||
this._clientSecret,
|
||||
const requestTokenResult = await this.requestEmailTokenCallback(
|
||||
this.inputs.emailAddress,
|
||||
this.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
this._data.session,
|
||||
this.data.session,
|
||||
);
|
||||
this._emailSid = requestTokenResult.sid;
|
||||
this.emailSid = requestTokenResult.sid;
|
||||
// NB. promise is not resolved here - at some point, doRequest
|
||||
// will be called again and if the user has jumped through all
|
||||
// the hoops correctly, auth will be complete and the request
|
||||
@@ -404,15 +482,14 @@ InteractiveAuth.prototype = {
|
||||
// to do) or it could be a network failure. Either way, pass
|
||||
// the failure up as the user can't complete auth if we can't
|
||||
// send the email, for whatever reason.
|
||||
this._rejectFunc(e);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
this.attemptAuthDeferred.reject(e);
|
||||
this.attemptAuthDeferred = null;
|
||||
} finally {
|
||||
this._requestingEmailToken = false;
|
||||
this.requestingEmailToken = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the next stage and call the callback
|
||||
@@ -420,34 +497,34 @@ InteractiveAuth.prototype = {
|
||||
* @private
|
||||
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
_startNextAuthStage: function() {
|
||||
const nextStage = this._chooseStage();
|
||||
private startNextAuthStage(): void {
|
||||
const nextStage = this.chooseStage();
|
||||
if (!nextStage) {
|
||||
throw new Error("No incomplete flows from the server");
|
||||
}
|
||||
this._currentStage = nextStage;
|
||||
this.currentStage = nextStage;
|
||||
|
||||
if (nextStage === 'm.login.dummy') {
|
||||
if (nextStage === AuthType.Dummy) {
|
||||
this.submitAuthDict({
|
||||
type: 'm.login.dummy',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._data && this._data.errcode || this._data.error) {
|
||||
this._stateUpdatedCallback(nextStage, {
|
||||
errcode: this._data.errcode || "",
|
||||
error: this._data.error || "",
|
||||
if (this.data && this.data.errcode || this.data.error) {
|
||||
this.stateUpdatedCallback(nextStage, {
|
||||
errcode: this.data.errcode || "",
|
||||
error: this.data.error || "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stageStatus = {};
|
||||
const stageStatus: IStageStatus = {};
|
||||
if (nextStage == EMAIL_STAGE_TYPE) {
|
||||
stageStatus.emailSid = this._emailSid;
|
||||
stageStatus.emailSid = this.emailSid;
|
||||
}
|
||||
this._stateUpdatedCallback(nextStage, stageStatus);
|
||||
},
|
||||
this.stateUpdatedCallback(nextStage, stageStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the next auth stage
|
||||
@@ -456,15 +533,15 @@ InteractiveAuth.prototype = {
|
||||
* @return {string?} login type
|
||||
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
_chooseStage: function() {
|
||||
if (this._chosenFlow === null) {
|
||||
this._chosenFlow = this._chooseFlow();
|
||||
private chooseStage(): AuthType {
|
||||
if (this.chosenFlow === null) {
|
||||
this.chosenFlow = this.chooseFlow();
|
||||
}
|
||||
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow));
|
||||
const nextStage = this._firstUncompletedStage(this._chosenFlow);
|
||||
logger.log("Active flow => %s", JSON.stringify(this.chosenFlow));
|
||||
const nextStage = this.firstUncompletedStage(this.chosenFlow);
|
||||
logger.log("Next stage: %s", nextStage);
|
||||
return nextStage;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick one of the flows from the returned list
|
||||
@@ -472,7 +549,7 @@ InteractiveAuth.prototype = {
|
||||
* be returned, otherwise, null will be returned.
|
||||
*
|
||||
* Only flows using all given inputs are chosen because it
|
||||
* is likley to be surprising if the user provides a
|
||||
* is likely to be surprising if the user provides a
|
||||
* credential and it is not used. For example, for registration,
|
||||
* this could result in the email not being used which would leave
|
||||
* the account with no means to reset a password.
|
||||
@@ -481,14 +558,14 @@ InteractiveAuth.prototype = {
|
||||
* @return {object} flow
|
||||
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
_chooseFlow: function() {
|
||||
const flows = this._data.flows || [];
|
||||
private chooseFlow(): IFlow {
|
||||
const flows = this.data.flows || [];
|
||||
|
||||
// we've been given an email or we've already done an email part
|
||||
const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid);
|
||||
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
|
||||
const haveMsisdn = (
|
||||
Boolean(this._inputs.phoneCountry) &&
|
||||
Boolean(this._inputs.phoneNumber)
|
||||
Boolean(this.inputs.phoneCountry) &&
|
||||
Boolean(this.inputs.phoneNumber)
|
||||
);
|
||||
|
||||
for (const flow of flows) {
|
||||
@@ -506,16 +583,14 @@ InteractiveAuth.prototype = {
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
|
||||
const requiredStages: string[] = [];
|
||||
if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE);
|
||||
if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE);
|
||||
// Throw an error with a fairly generic description, but with more
|
||||
// information such that the app can give a better one if so desired.
|
||||
const err = new Error("No appropriate authentication flow found");
|
||||
err.name = 'NoAuthFlowFoundError';
|
||||
err.required_stages = [];
|
||||
if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE);
|
||||
if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE);
|
||||
err.available_flows = flows;
|
||||
throw err;
|
||||
},
|
||||
throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first uncompleted stage in the given flow
|
||||
@@ -524,14 +599,13 @@ InteractiveAuth.prototype = {
|
||||
* @param {object} flow
|
||||
* @return {string} login type
|
||||
*/
|
||||
_firstUncompletedStage: function(flow) {
|
||||
const completed = (this._data || {}).completed || [];
|
||||
private firstUncompletedStage(flow: IFlow): AuthType {
|
||||
const completed = this.data.completed || [];
|
||||
for (let i = 0; i < flow.stages.length; ++i) {
|
||||
const stageType = flow.stages[i];
|
||||
if (completed.indexOf(stageType) === -1) {
|
||||
return stageType;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -60,7 +60,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
* Can be tailored down to specific use cases if needed.
|
||||
*/
|
||||
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
|
||||
logger.setLevel(log.levels.DEBUG);
|
||||
logger.setLevel(log.levels.DEBUG, false);
|
||||
|
||||
export interface PrefixedLogger extends Logger {
|
||||
withPrefix?: (prefix: string) => PrefixedLogger;
|
||||
@@ -82,7 +82,7 @@ function getPrefixedLogger(prefix): PrefixedLogger {
|
||||
// Only do this setup work the first time through, as loggers are saved by name.
|
||||
extendLogger(prefixLogger);
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.setLevel(log.levels.DEBUG);
|
||||
prefixLogger.setLevel(log.levels.DEBUG, false);
|
||||
}
|
||||
return prefixLogger;
|
||||
}
|
||||
|
||||
@@ -49,8 +49,6 @@ export * from "./content-repo";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
setAudioInput as setMatrixCallAudioInput,
|
||||
setVideoInput as setMatrixCallVideoInput,
|
||||
} from "./webrtc/call";
|
||||
|
||||
// expose the underlying request object so different environments can use
|
||||
|
||||
@@ -77,6 +77,26 @@ export class MSC3089Branch {
|
||||
}, this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether or not a file is locked.
|
||||
* @returns {boolean} True if locked, false otherwise.
|
||||
*/
|
||||
public isLocked(): boolean {
|
||||
return this.indexEvent.getContent()['locked'] || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a file as locked or unlocked.
|
||||
* @param {boolean} locked True to lock the file, false otherwise.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public async setLocked(locked: boolean): Promise<void> {
|
||||
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||
...this.indexEvent.getContent(),
|
||||
locked: locked,
|
||||
}, this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the file needed to download it.
|
||||
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file.
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MatrixClient } from "../client";
|
||||
import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
|
||||
import { Room } from "./room";
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { IContent, MatrixEvent } from "./event";
|
||||
import {
|
||||
averageBetweenStrings,
|
||||
DEFAULT_ALPHABET,
|
||||
@@ -451,11 +451,14 @@ export class MSC3089TreeSpace {
|
||||
* @param {string} name The name of the file.
|
||||
* @param {ArrayBuffer} 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 createFile(
|
||||
name: string,
|
||||
encryptedContents: ArrayBuffer, info: Partial<IEncryptedFile>,
|
||||
encryptedContents: ArrayBuffer,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<void> {
|
||||
const mxc = await this.client.uploadContent(new Blob([encryptedContents]), {
|
||||
includeFilename: false,
|
||||
@@ -464,6 +467,7 @@ export class MSC3089TreeSpace {
|
||||
info.url = mxc;
|
||||
|
||||
const res = await this.client.sendMessage(this.roomId, {
|
||||
...(additionalContent ?? {}),
|
||||
msgtype: MsgType.File,
|
||||
body: name,
|
||||
url: mxc,
|
||||
|
||||
+73
-8
@@ -24,10 +24,18 @@ import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
import { EventType, MsgType, RelationType } from "../@types/event";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
|
||||
} from "../@types/event";
|
||||
import { Crypto } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { IActionsObject } from '../pushprocessor';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
@@ -148,7 +156,7 @@ export interface IDecryptOptions {
|
||||
}
|
||||
|
||||
export class MatrixEvent extends EventEmitter {
|
||||
private pushActions: object = null;
|
||||
private pushActions: IActionsObject = null;
|
||||
private _replacingEvent: MatrixEvent = null;
|
||||
private _localRedactionEvent: MatrixEvent = null;
|
||||
private _isCancelled = false;
|
||||
@@ -190,6 +198,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
private txnId: string = null;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread: Thread = null;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
@@ -211,6 +225,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public verificationRequest = null;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
* @constructor
|
||||
@@ -260,6 +276,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
this.txnId = event.txn_id || null;
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,6 +397,34 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.event.content || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the event ID of the replied event
|
||||
*/
|
||||
public get replyEventId(): string {
|
||||
const relations = this.getWireContent()["m.relates_to"];
|
||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Determines whether a reply should be rendered in a thread
|
||||
* or in the main room timeline
|
||||
*/
|
||||
public get replyInThread(): boolean {
|
||||
const replyTo = this.getWireContent()
|
||||
?.["m.relates_to"]
|
||||
?.["m.in_reply_to"];
|
||||
return (this.replyEventId
|
||||
&& replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|
||||
|| this.thread instanceof Thread;
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId
|
||||
|| this.getWireContent()["m.relates_to"]?.event_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous event content JSON. This will only return something for
|
||||
* state events which exist in the timeline.
|
||||
@@ -947,7 +992,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
*
|
||||
* @return {?Object} push actions
|
||||
*/
|
||||
public getPushActions(): object | null {
|
||||
public getPushActions(): IActionsObject | null {
|
||||
return this.pushActions;
|
||||
}
|
||||
|
||||
@@ -956,7 +1001,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
*
|
||||
* @param {Object} pushActions push actions
|
||||
*/
|
||||
public setPushActions(pushActions: object): void {
|
||||
public setPushActions(pushActions: IActionsObject): void {
|
||||
this.pushActions = pushActions;
|
||||
}
|
||||
|
||||
@@ -1234,10 +1279,15 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarise the event as JSON for debugging. If encrypted, include both the
|
||||
* decrypted and encrypted view of the event. This is named `toJSON` for use
|
||||
* with `JSON.stringify` which checks objects for functions named `toJSON`
|
||||
* and will call them to customise the output if they are defined.
|
||||
* Summarise the event as JSON. This is currently used by React SDK's view
|
||||
* event source feature and Seshat's event indexing, so take care when
|
||||
* adjusting the output here.
|
||||
*
|
||||
* If encrypted, include both the decrypted and encrypted view of the event.
|
||||
*
|
||||
* This is named `toJSON` for use with `JSON.stringify` which checks objects
|
||||
* for functions named `toJSON` and will call them to customise the output
|
||||
* if they are defined.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
@@ -1265,6 +1315,21 @@ export class MatrixEvent extends EventEmitter {
|
||||
public getTxnId(): string | undefined {
|
||||
return this.txnId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public setThread(thread: Thread): void {
|
||||
this.thread = thread;
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(): Thread {
|
||||
return this.thread;
|
||||
}
|
||||
}
|
||||
|
||||
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
/**
|
||||
* @module models/group
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
|
||||
import * as utils from "../utils";
|
||||
@@ -34,6 +35,7 @@ import { EventEmitter } from "events";
|
||||
* @prop {Object} inviter Infomation about the user who invited the logged in user
|
||||
* to the group, if myMembership is 'invite'.
|
||||
* @prop {string} inviter.userId The user ID of the inviter
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
export function Group(groupId) {
|
||||
this.groupId = groupId;
|
||||
@@ -76,6 +78,7 @@ Group.prototype.setInviter = function(inviter) {
|
||||
* This means the 'name' and 'avatarUrl' properties.
|
||||
* @event module:client~MatrixClient#"Group.profile"
|
||||
* @param {Group} group The group whose profile was updated.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
* @example
|
||||
* matrixClient.on("Group.profile", function(group){
|
||||
* var name = group.name;
|
||||
@@ -87,6 +90,7 @@ Group.prototype.setInviter = function(inviter) {
|
||||
* the group is updated.
|
||||
* @event module:client~MatrixClient#"Group.myMembership"
|
||||
* @param {Group} group The group in which the user's membership changed
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
* @example
|
||||
* matrixClient.on("Group.myMembership", function(group){
|
||||
* var myMembership = group.myMembership;
|
||||
|
||||
@@ -103,7 +103,7 @@ export class RoomMember extends EventEmitter {
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.name"
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.membership"
|
||||
*/
|
||||
public setMembershipEvent(event: MatrixEvent, roomState: RoomState): void {
|
||||
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
|
||||
const displayName = event.getDirectionalContent().displayname;
|
||||
|
||||
if (event.getType() !== "m.room.member") {
|
||||
@@ -322,7 +322,7 @@ export class RoomMember extends EventEmitter {
|
||||
const MXID_PATTERN = /@.+:.+/;
|
||||
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||
|
||||
function shouldDisambiguate(selfUserId: string, displayName: string, roomState: RoomState): boolean {
|
||||
function shouldDisambiguate(selfUserId: string, displayName: string, roomState?: RoomState): boolean {
|
||||
if (!displayName || displayName === selfUserId) return false;
|
||||
|
||||
// First check if the displayname is something we consider truthy
|
||||
|
||||
@@ -26,6 +26,7 @@ import * as utils from "../utils";
|
||||
import { EventType } from "../@types/event";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
enum OobStatus {
|
||||
@@ -728,10 +729,30 @@ export class RoomState extends EventEmitter {
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
public getJoinRule(): string {
|
||||
public getJoinRule(): JoinRule {
|
||||
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
|
||||
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
|
||||
return joinRuleContent["join_rule"] || "invite";
|
||||
const joinRuleContent = joinRuleEvent?.getContent<IJoinRuleEventContent>() ?? {};
|
||||
return joinRuleContent["join_rule"] || JoinRule.Invite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
|
||||
* @returns {HistoryVisibility} the history_visibility applied to this room
|
||||
*/
|
||||
public getHistoryVisibility(): HistoryVisibility {
|
||||
const historyVisibilityEvent = this.getStateEvents(EventType.RoomHistoryVisibility, "");
|
||||
const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {};
|
||||
return historyVisibilityContent["history_visibility"] || HistoryVisibility.Shared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`.
|
||||
* @returns {GuestAccess} the guest_access applied to this room
|
||||
*/
|
||||
public getGuestAccess(): GuestAccess {
|
||||
const guestAccessEvent = this.getStateEvents(EventType.RoomGuestAccess, "");
|
||||
const guestAccessContent = guestAccessEvent?.getContent() ?? {};
|
||||
return guestAccessContent["guest_access"] || GuestAccess.Forbidden;
|
||||
}
|
||||
|
||||
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
|
||||
|
||||
+127
-4
@@ -32,9 +32,10 @@ import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
import { ResizeMethod } from "../@types/partials";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -145,6 +146,11 @@ export class Room extends EventEmitter {
|
||||
public oldState: RoomState;
|
||||
public currentState: RoomState;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Set<Thread>();
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
*
|
||||
@@ -857,13 +863,26 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an event which is stored in our unfiltered timeline set
|
||||
* Get an event which is stored in our unfiltered timeline set or in a thread
|
||||
*
|
||||
* @param {string} eventId event ID to look for
|
||||
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
||||
*/
|
||||
public findEventById(eventId: string): MatrixEvent | undefined {
|
||||
return this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||
|
||||
if (event) {
|
||||
return event;
|
||||
} else {
|
||||
const threads = this.getThreads();
|
||||
for (let i = 0; i < threads.length; i++) {
|
||||
const thread = threads[i];
|
||||
event = thread.findEventById(eventId);
|
||||
if (event) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1049,6 +1068,55 @@ export class Room extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public addThread(thread: Thread): Set<Thread> {
|
||||
this.threads.add(thread);
|
||||
if (!thread.ready) {
|
||||
thread.once(ThreadEvent.Ready, this.dedupeThreads);
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
|
||||
}
|
||||
return this.threads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(eventId: string): Thread {
|
||||
return this.getThreads().find(thread => {
|
||||
return thread.id === eventId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThreads(): Thread[] {
|
||||
return Array.from(this.threads.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Two threads starting from a different child event can end up
|
||||
* with the same event root. This method ensures that the duplicates
|
||||
* are removed
|
||||
* @experimental
|
||||
*/
|
||||
private dedupeThreads = (readyThread): void => {
|
||||
const deduped = Array.from(this.threads).reduce((dedupedThreads, thread) => {
|
||||
if (dedupedThreads.has(thread.id)) {
|
||||
dedupedThreads.get(thread.id).merge(thread);
|
||||
} else {
|
||||
dedupedThreads.set(thread.id, thread);
|
||||
}
|
||||
|
||||
return dedupedThreads;
|
||||
}, new Map<string, Thread>());
|
||||
|
||||
this.threads = new Set<Thread>(deduped.values());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a member from the current room state.
|
||||
* @param {string} userId The user ID of the member.
|
||||
@@ -1225,6 +1293,30 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public addThreadedEvent(event: MatrixEvent): void {
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.handleRemoteEcho(event, existingEvent);
|
||||
}
|
||||
}
|
||||
let thread = this.findEventById(event.parentEventId)?.getThread();
|
||||
if (thread) {
|
||||
thread.addEvent(event);
|
||||
} else {
|
||||
thread = new Thread([event], this, this.client);
|
||||
}
|
||||
|
||||
if (!this.threads.has(thread)) {
|
||||
this.addThread(thread);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the end of this room's live timelines. Will fire
|
||||
* "Room.timeline".
|
||||
@@ -1321,6 +1413,13 @@ export class Room extends EventEmitter {
|
||||
* unique transaction id.
|
||||
*/
|
||||
public addPendingEvent(event: MatrixEvent, txnId: string): void {
|
||||
// TODO: Enable "pending events" for threads
|
||||
// There's a fair few things to update to make them work with Threads
|
||||
// Will get back to it when the plan is to build a more polished UI ready for production
|
||||
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
|
||||
throw new Error("addPendingEvent called on an event with status " +
|
||||
event.status);
|
||||
@@ -1493,6 +1592,14 @@ export class Room extends EventEmitter {
|
||||
oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
public findThreadByEventId(eventId: string): Thread {
|
||||
for (const thread of this.threads) {
|
||||
if (thread.has(eventId)) {
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status / event id on a pending event, to reflect its transmission
|
||||
* progress.
|
||||
@@ -1973,10 +2080,26 @@ export class Room extends EventEmitter {
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
public getJoinRule(): string {
|
||||
public getJoinRule(): JoinRule {
|
||||
return this.currentState.getJoinRule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
|
||||
* @returns {HistoryVisibility} the history_visibility applied to this room
|
||||
*/
|
||||
public getHistoryVisibility(): HistoryVisibility {
|
||||
return this.currentState.getHistoryVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
|
||||
* @returns {HistoryVisibility} the history_visibility applied to this room
|
||||
*/
|
||||
public getGuestAccess(): GuestAccess {
|
||||
return this.currentState.getGuestAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
|
||||
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
|
||||
|
||||
@@ -31,7 +31,7 @@ export class SearchResult {
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
||||
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
||||
const jsonContext = jsonObj.context || {} as IResultContext;
|
||||
const eventsBefore = jsonContext.events_before || [];
|
||||
const eventsAfter = jsonContext.events_after || [];
|
||||
@@ -57,4 +57,3 @@ export class SearchResult {
|
||||
*/
|
||||
constructor(public readonly rank: number, public readonly context: EventContext) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { MatrixClient } from "../matrix";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
|
||||
export enum ThreadEvent {
|
||||
Ready = "Thread.ready",
|
||||
Update = "Thread.update"
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends EventEmitter {
|
||||
/**
|
||||
* A reference to the event ID at the top of the thread
|
||||
*/
|
||||
private root: string;
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
public readonly tail = new Set<string>();
|
||||
public readonly timelineSet: EventTimelineSet;
|
||||
|
||||
constructor(
|
||||
events: MatrixEvent[] = [],
|
||||
public readonly room: Room,
|
||||
public readonly client: MatrixClient,
|
||||
) {
|
||||
super();
|
||||
this.timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
});
|
||||
events.forEach(event => this.addEvent(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
* Will fire "Thread.update"
|
||||
* @param event The event to add
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent): Promise<void> {
|
||||
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tail.has(event.replyEventId)) {
|
||||
this.tail.delete(event.replyEventId);
|
||||
}
|
||||
this.tail.add(event.getId());
|
||||
|
||||
if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
|
||||
this.root = event.getId();
|
||||
}
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addLiveEvent(event);
|
||||
|
||||
if (this.ready) {
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
}
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the reply chain with all events
|
||||
* missing from the current sync data
|
||||
* Will fire "Thread.ready"
|
||||
*/
|
||||
public async fetchReplyChain(): Promise<void> {
|
||||
if (!this.ready) {
|
||||
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
|
||||
if (!mxEvent) {
|
||||
mxEvent = await this.fetchEventById(
|
||||
this.rootEvent.getRoomId(),
|
||||
this.rootEvent.replyEventId,
|
||||
);
|
||||
}
|
||||
|
||||
this.addEvent(mxEvent);
|
||||
if (mxEvent.replyEventId) {
|
||||
await this.fetchReplyChain();
|
||||
} else {
|
||||
await this.decryptEvents();
|
||||
this.emit(ThreadEvent.Ready, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptEvents(): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
|
||||
return this.client.decryptEventIfNeeded(event, {});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an event over the network
|
||||
*/
|
||||
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
|
||||
const response = await this.client.http.authedRequest(
|
||||
undefined,
|
||||
"GET",
|
||||
`/rooms/${roomId}/event/${eventId}`,
|
||||
);
|
||||
return new MatrixEvent(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an event by ID in the current thread
|
||||
*/
|
||||
public findEventById(eventId: string) {
|
||||
return this.timelineSet.findEventById(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines thread's ready status
|
||||
*/
|
||||
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 get roomId(): string {
|
||||
return this.rootEvent.getRoomId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of messages in the thread
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
const events = this.events;
|
||||
return events[events.length -1];
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public has(eventId: string): boolean {
|
||||
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
|
||||
}
|
||||
|
||||
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.once(event, listener);
|
||||
return this;
|
||||
}
|
||||
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.off(event, listener);
|
||||
return this;
|
||||
}
|
||||
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.addListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||
super.removeListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2015 - 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.
|
||||
@@ -17,12 +16,36 @@ limitations under the License.
|
||||
|
||||
import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { MatrixClient } from "./client";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import {
|
||||
ConditionKind,
|
||||
IAnnotatedPushRule,
|
||||
IContainsDisplayNameCondition,
|
||||
IEventMatchCondition,
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
IRoomMemberCountCondition,
|
||||
ISenderNotificationPermissionCondition,
|
||||
PushRuleAction,
|
||||
PushRuleActionName,
|
||||
PushRuleCondition,
|
||||
PushRuleKind,
|
||||
PushRuleSet,
|
||||
TweakName,
|
||||
} from "./@types/PushRules";
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
*/
|
||||
|
||||
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
||||
const RULEKINDS_IN_ORDER = [
|
||||
PushRuleKind.Override,
|
||||
PushRuleKind.ContentSpecific,
|
||||
PushRuleKind.RoomSpecific,
|
||||
PushRuleKind.SenderSpecific,
|
||||
PushRuleKind.Underride,
|
||||
];
|
||||
|
||||
// The default override rules to apply to the push rules that arrive from the server.
|
||||
// We do this for two reasons:
|
||||
@@ -31,7 +54,7 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
|
||||
// more details.
|
||||
// 2. We often want to start using push rules ahead of the server supporting them,
|
||||
// and so we can put them here.
|
||||
const DEFAULT_OVERRIDE_RULES = [
|
||||
const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
||||
{
|
||||
// For homeservers which don't support MSC1930 yet
|
||||
rule_id: ".m.rule.tombstone",
|
||||
@@ -39,20 +62,20 @@ const DEFAULT_OVERRIDE_RULES = [
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "m.room.tombstone",
|
||||
},
|
||||
{
|
||||
kind: "event_match",
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "state_key",
|
||||
pattern: "",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
set_tweak: TweakName.Highlight,
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
@@ -64,31 +87,97 @@ const DEFAULT_OVERRIDE_RULES = [
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "m.reaction",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"dont_notify",
|
||||
PushRuleActionName.DontNotify,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Construct a Push Processor.
|
||||
* @constructor
|
||||
* @param {Object} client The Matrix client object to use
|
||||
*/
|
||||
export function PushProcessor(client) {
|
||||
const cachedGlobToRegex = {
|
||||
// $glob: RegExp,
|
||||
};
|
||||
export interface IActionsObject {
|
||||
notify: boolean;
|
||||
tweaks: Partial<Record<TweakName, any>>;
|
||||
}
|
||||
|
||||
const matchingRuleFromKindSet = (ev, kindset) => {
|
||||
for (let ruleKindIndex = 0;
|
||||
ruleKindIndex < RULEKINDS_IN_ORDER.length;
|
||||
++ruleKindIndex) {
|
||||
export class PushProcessor {
|
||||
/**
|
||||
* Construct a Push Processor.
|
||||
* @constructor
|
||||
* @param {Object} client The Matrix client object to use
|
||||
*/
|
||||
constructor(private readonly client: MatrixClient) {}
|
||||
|
||||
/**
|
||||
* Convert a list of actions into a object with the actions as keys and their values
|
||||
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
|
||||
* becomes { notify: true, tweaks: { sound: 'default' } }
|
||||
* @param {array} actionList The actions list
|
||||
*
|
||||
* @return {object} A object with key 'notify' (true or false) and an object of actions
|
||||
*/
|
||||
public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject {
|
||||
const actionObj: IActionsObject = { notify: false, tweaks: {} };
|
||||
for (let i = 0; i < actionList.length; ++i) {
|
||||
const action = actionList[i];
|
||||
if (action === PushRuleActionName.Notify) {
|
||||
actionObj.notify = true;
|
||||
} else if (typeof action === 'object') {
|
||||
if (action.value === undefined) {
|
||||
action.value = true;
|
||||
}
|
||||
actionObj.tweaks[action.set_tweak] = action.value;
|
||||
}
|
||||
}
|
||||
return actionObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites conditions on a client's push rules to match the defaults
|
||||
* where applicable. Useful for upgrading push rules to more strict
|
||||
* conditions when the server is falling behind on defaults.
|
||||
* @param {object} incomingRules The client's existing push rules
|
||||
* @returns {object} The rewritten rules
|
||||
*/
|
||||
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
|
||||
let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
|
||||
|
||||
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||
// properties missing in practice.
|
||||
if (!newRules) newRules = {} as IPushRules;
|
||||
if (!newRules.global) newRules.global = {} as PushRuleSet;
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
|
||||
// Merge the client-level defaults with the ones from the server
|
||||
const globalOverrides = newRules.global.override;
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
existingRule.default = override.default;
|
||||
existingRule.conditions = override.conditions;
|
||||
existingRule.actions = override.actions;
|
||||
} else {
|
||||
// Add the rule
|
||||
const ruleId = override.rule_id;
|
||||
logger.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
return newRules;
|
||||
}
|
||||
|
||||
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
|
||||
|
||||
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule {
|
||||
for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) {
|
||||
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
|
||||
const ruleset = kindset[kind];
|
||||
if (!ruleset) {
|
||||
@@ -101,89 +190,96 @@ export function PushProcessor(client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawrule = templateRuleToRaw(kind, rule);
|
||||
const rawrule = this.templateRuleToRaw(kind, rule);
|
||||
if (!rawrule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.ruleMatchesEvent(rawrule, ev)) {
|
||||
rule.kind = kind;
|
||||
return rule;
|
||||
return {
|
||||
...rule,
|
||||
kind,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const templateRuleToRaw = function(kind, tprule) {
|
||||
private templateRuleToRaw(kind: PushRuleKind, tprule: any): any {
|
||||
const rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
'conditions': [],
|
||||
};
|
||||
switch (kind) {
|
||||
case 'underride':
|
||||
case 'override':
|
||||
case PushRuleKind.Underride:
|
||||
case PushRuleKind.Override:
|
||||
rawrule.conditions = tprule.conditions;
|
||||
break;
|
||||
case 'room':
|
||||
case PushRuleKind.RoomSpecific:
|
||||
if (!tprule.rule_id) {
|
||||
return null;
|
||||
}
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'kind': ConditionKind.EventMatch,
|
||||
'key': 'room_id',
|
||||
'value': tprule.rule_id,
|
||||
});
|
||||
break;
|
||||
case 'sender':
|
||||
case PushRuleKind.SenderSpecific:
|
||||
if (!tprule.rule_id) {
|
||||
return null;
|
||||
}
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'kind': ConditionKind.EventMatch,
|
||||
'key': 'user_id',
|
||||
'value': tprule.rule_id,
|
||||
});
|
||||
break;
|
||||
case 'content':
|
||||
case PushRuleKind.ContentSpecific:
|
||||
if (!tprule.pattern) {
|
||||
return null;
|
||||
}
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'kind': ConditionKind.EventMatch,
|
||||
'key': 'content.body',
|
||||
'pattern': tprule.pattern,
|
||||
});
|
||||
break;
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
}
|
||||
|
||||
const eventFulfillsCondition = function(cond, ev) {
|
||||
const condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition,
|
||||
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
|
||||
};
|
||||
if (condition_functions[cond.kind]) {
|
||||
return condition_functions[cond.kind](cond, ev);
|
||||
private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean {
|
||||
switch (cond.kind) {
|
||||
case ConditionKind.EventMatch:
|
||||
return this.eventFulfillsEventMatchCondition(cond, ev);
|
||||
case ConditionKind.ContainsDisplayName:
|
||||
return this.eventFulfillsDisplayNameCondition(cond, ev);
|
||||
case ConditionKind.RoomMemberCount:
|
||||
return this.eventFulfillsRoomMemberCountCondition(cond, ev);
|
||||
case ConditionKind.SenderNotificationPermission:
|
||||
return this.eventFulfillsSenderNotifPermCondition(cond, ev);
|
||||
}
|
||||
|
||||
// unknown conditions: we previously matched all unknown conditions,
|
||||
// but given that rules can be added to the base rules on a server,
|
||||
// it's probably better to not match unknown conditions.
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const eventFulfillsSenderNotifPermCondition = function(cond, ev) {
|
||||
private eventFulfillsSenderNotifPermCondition(
|
||||
cond: ISenderNotificationPermissionCondition,
|
||||
ev: MatrixEvent,
|
||||
): boolean {
|
||||
const notifLevelKey = cond['key'];
|
||||
if (!notifLevelKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState) {
|
||||
const room = this.client.getRoom(ev.getRoomId());
|
||||
if (!room?.currentState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,14 +287,14 @@ export function PushProcessor(client) {
|
||||
// the point the event is in the DAG. Unfortunately the js-sdk does not store
|
||||
// this.
|
||||
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
|
||||
};
|
||||
}
|
||||
|
||||
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||||
private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.is) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = client.getRoom(ev.getRoomId());
|
||||
const room = this.client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members) {
|
||||
return false;
|
||||
}
|
||||
@@ -229,9 +325,9 @@ export function PushProcessor(client) {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
|
||||
let content = ev.getContent();
|
||||
if (ev.isEncrypted() && ev.getClearContent()) {
|
||||
content = ev.getClearContent();
|
||||
@@ -240,26 +336,26 @@ export function PushProcessor(client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = client.getRoom(ev.getRoomId());
|
||||
const room = this.client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members ||
|
||||
!room.currentState.getMember(client.credentials.userId)) {
|
||||
!room.currentState.getMember(this.client.credentials.userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const displayName = room.currentState.getMember(client.credentials.userId).name;
|
||||
const displayName = room.currentState.getMember(this.client.credentials.userId).name;
|
||||
|
||||
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
|
||||
// as shorthand for [^0-9A-Za-z_].
|
||||
const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
}
|
||||
|
||||
const eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const val = valueForDottedKey(cond.key, ev);
|
||||
const val = this.valueForDottedKey(cond.key, ev);
|
||||
if (typeof val !== 'string') {
|
||||
return false;
|
||||
}
|
||||
@@ -275,26 +371,26 @@ export function PushProcessor(client) {
|
||||
let regex;
|
||||
|
||||
if (cond.key == 'content.body') {
|
||||
regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
|
||||
regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
|
||||
} else {
|
||||
regex = createCachedRegex('^', cond.pattern, '$');
|
||||
regex = this.createCachedRegex('^', cond.pattern, '$');
|
||||
}
|
||||
|
||||
return !!val.match(regex);
|
||||
};
|
||||
}
|
||||
|
||||
const createCachedRegex = function(prefix, glob, suffix) {
|
||||
if (cachedGlobToRegex[glob]) {
|
||||
return cachedGlobToRegex[glob];
|
||||
private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp {
|
||||
if (PushProcessor.cachedGlobToRegex[glob]) {
|
||||
return PushProcessor.cachedGlobToRegex[glob];
|
||||
}
|
||||
cachedGlobToRegex[glob] = new RegExp(
|
||||
PushProcessor.cachedGlobToRegex[glob] = new RegExp(
|
||||
prefix + globToRegexp(glob) + suffix,
|
||||
'i', // Case insensitive
|
||||
);
|
||||
return cachedGlobToRegex[glob];
|
||||
};
|
||||
return PushProcessor.cachedGlobToRegex[glob];
|
||||
}
|
||||
|
||||
const valueForDottedKey = function(key, ev) {
|
||||
private valueForDottedKey(key: string, ev: MatrixEvent): any {
|
||||
const parts = key.split('.');
|
||||
let val;
|
||||
|
||||
@@ -319,23 +415,23 @@ export function PushProcessor(client) {
|
||||
val = val[thisPart];
|
||||
}
|
||||
return val;
|
||||
};
|
||||
}
|
||||
|
||||
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule {
|
||||
if (!rulesets) {
|
||||
return null;
|
||||
}
|
||||
if (ev.getSender() === client.credentials.userId) {
|
||||
if (ev.getSender() === this.client.credentials.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
return this.matchingRuleFromKindSet(ev, rulesets.global);
|
||||
}
|
||||
|
||||
const pushActionsForEventAndRulesets = function(ev, rulesets) {
|
||||
const rule = matchingRuleForEventWithRulesets(ev, rulesets);
|
||||
private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject {
|
||||
const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
|
||||
if (!rule) {
|
||||
return {};
|
||||
return {} as IActionsObject;
|
||||
}
|
||||
|
||||
const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
|
||||
@@ -344,21 +440,22 @@ export function PushProcessor(client) {
|
||||
if (actionObj.tweaks.highlight === undefined) {
|
||||
// if it isn't specified, highlight if it's a content
|
||||
// rule but otherwise not
|
||||
actionObj.tweaks.highlight = (rule.kind == 'content');
|
||||
actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific);
|
||||
}
|
||||
|
||||
return actionObj;
|
||||
};
|
||||
}
|
||||
|
||||
this.ruleMatchesEvent = function(rule, ev) {
|
||||
public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean {
|
||||
let ret = true;
|
||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||
const cond = rule.conditions[i];
|
||||
ret &= eventFulfillsCondition(cond, ev);
|
||||
// @ts-ignore
|
||||
ret &= this.eventFulfillsCondition(cond, ev);
|
||||
}
|
||||
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's push actions for the given event
|
||||
@@ -367,9 +464,9 @@ export function PushProcessor(client) {
|
||||
*
|
||||
* @return {PushAction}
|
||||
*/
|
||||
this.actionsForEvent = function(ev) {
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
};
|
||||
public actionsForEvent(ev: MatrixEvent): IActionsObject {
|
||||
return this.pushActionsForEventAndRulesets(ev, this.client.pushRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one of the users push rules by its ID
|
||||
@@ -377,85 +474,22 @@ export function PushProcessor(client) {
|
||||
* @param {string} ruleId The ID of the rule to search for
|
||||
* @return {object} The push rule, or null if no such rule was found
|
||||
*/
|
||||
this.getPushRuleById = function(ruleId) {
|
||||
public getPushRuleById(ruleId: string): IPushRule {
|
||||
for (const scope of ['global']) {
|
||||
if (client.pushRules[scope] === undefined) continue;
|
||||
if (this.client.pushRules[scope] === undefined) continue;
|
||||
|
||||
for (const kind of RULEKINDS_IN_ORDER) {
|
||||
if (client.pushRules[scope][kind] === undefined) continue;
|
||||
if (this.client.pushRules[scope][kind] === undefined) continue;
|
||||
|
||||
for (const rule of client.pushRules[scope][kind]) {
|
||||
for (const rule of this.client.pushRules[scope][kind]) {
|
||||
if (rule.rule_id === ruleId) return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of actions into a object with the actions as keys and their values
|
||||
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
|
||||
* becomes { notify: true, tweaks: { sound: 'default' } }
|
||||
* @param {array} actionlist The actions list
|
||||
*
|
||||
* @return {object} A object with key 'notify' (true or false) and an object of actions
|
||||
*/
|
||||
PushProcessor.actionListToActionsObject = function(actionlist) {
|
||||
const actionobj = { 'notify': false, 'tweaks': {} };
|
||||
for (let i = 0; i < actionlist.length; ++i) {
|
||||
const action = actionlist[i];
|
||||
if (action === 'notify') {
|
||||
actionobj.notify = true;
|
||||
} else if (typeof action === 'object') {
|
||||
if (action.value === undefined) {
|
||||
action.value = true;
|
||||
}
|
||||
actionobj.tweaks[action.set_tweak] = action.value;
|
||||
}
|
||||
}
|
||||
return actionobj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites conditions on a client's push rules to match the defaults
|
||||
* where applicable. Useful for upgrading push rules to more strict
|
||||
* conditions when the server is falling behind on defaults.
|
||||
* @param {object} incomingRules The client's existing push rules
|
||||
* @returns {object} The rewritten rules
|
||||
*/
|
||||
PushProcessor.rewriteDefaultRules = function(incomingRules) {
|
||||
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
|
||||
|
||||
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||
// properties missing in practice.
|
||||
if (!newRules) newRules = {};
|
||||
if (!newRules.global) newRules.global = {};
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
|
||||
// Merge the client-level defaults with the ones from the server
|
||||
const globalOverrides = newRules.global.override;
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
existingRule.default = override.default;
|
||||
existingRule.conditions = override.conditions;
|
||||
existingRule.actions = override.actions;
|
||||
} else {
|
||||
// Add the rule
|
||||
const ruleId = override.rule_id;
|
||||
logger.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
return newRules;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} PushAction
|
||||
* @type {Object}
|
||||
@@ -31,17 +31,22 @@ import { logger } from './logger';
|
||||
const TIMER_CHECK_PERIOD_MS = 1000;
|
||||
|
||||
// counter, for making up ids to return from setTimeout
|
||||
let _count = 0;
|
||||
let count = 0;
|
||||
|
||||
// the key for our callback with the real global.setTimeout
|
||||
let _realCallbackKey;
|
||||
let realCallbackKey: NodeJS.Timeout | number;
|
||||
|
||||
// a sorted list of the callbacks to be run.
|
||||
// each is an object with keys [runAt, func, params, key].
|
||||
const _callbackList = [];
|
||||
const callbackList: {
|
||||
runAt: number;
|
||||
func: (...params: any[]) => void;
|
||||
params: any[];
|
||||
key: number;
|
||||
}[] = [];
|
||||
|
||||
// var debuglog = logger.log.bind(logger);
|
||||
const debuglog = function() {};
|
||||
const debuglog = function(...params: any[]) {};
|
||||
|
||||
/**
|
||||
* Replace the function used by this module to get the current time.
|
||||
@@ -52,10 +57,10 @@ const debuglog = function() {};
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function setNow(f) {
|
||||
_now = f || Date.now;
|
||||
export function setNow(f: () => number): void {
|
||||
now = f || Date.now;
|
||||
}
|
||||
let _now = Date.now;
|
||||
let now = Date.now;
|
||||
|
||||
/**
|
||||
* reimplementation of window.setTimeout, which will call the callback if
|
||||
@@ -67,17 +72,16 @@ let _now = Date.now;
|
||||
* @return {Number} an identifier for this callback, which may be passed into
|
||||
* clearTimeout later.
|
||||
*/
|
||||
export function setTimeout(func, delayMs) {
|
||||
export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number {
|
||||
delayMs = delayMs || 0;
|
||||
if (delayMs < 0) {
|
||||
delayMs = 0;
|
||||
}
|
||||
|
||||
const params = Array.prototype.slice.call(arguments, 2);
|
||||
const runAt = _now() + delayMs;
|
||||
const key = _count++;
|
||||
const runAt = now() + delayMs;
|
||||
const key = count++;
|
||||
debuglog("setTimeout: scheduling cb", key, "at", runAt,
|
||||
"(delay", delayMs, ")");
|
||||
"(delay", delayMs, ")");
|
||||
const data = {
|
||||
runAt: runAt,
|
||||
func: func,
|
||||
@@ -87,13 +91,13 @@ export function setTimeout(func, delayMs) {
|
||||
|
||||
// figure out where it goes in the list
|
||||
const idx = binarySearch(
|
||||
_callbackList, function(el) {
|
||||
callbackList, function(el) {
|
||||
return el.runAt - runAt;
|
||||
},
|
||||
);
|
||||
|
||||
_callbackList.splice(idx, 0, data);
|
||||
_scheduleRealCallback();
|
||||
callbackList.splice(idx, 0, data);
|
||||
scheduleRealCallback();
|
||||
|
||||
return key;
|
||||
}
|
||||
@@ -103,68 +107,69 @@ export function setTimeout(func, delayMs) {
|
||||
*
|
||||
* @param {Number} key result from an earlier setTimeout call
|
||||
*/
|
||||
export function clearTimeout(key) {
|
||||
if (_callbackList.length === 0) {
|
||||
export function clearTimeout(key: number): void {
|
||||
if (callbackList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the element from the list
|
||||
let i;
|
||||
for (i = 0; i < _callbackList.length; i++) {
|
||||
const cb = _callbackList[i];
|
||||
for (i = 0; i < callbackList.length; i++) {
|
||||
const cb = callbackList[i];
|
||||
if (cb.key == key) {
|
||||
_callbackList.splice(i, 1);
|
||||
callbackList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// iff it was the first one in the list, reschedule our callback.
|
||||
if (i === 0) {
|
||||
_scheduleRealCallback();
|
||||
scheduleRealCallback();
|
||||
}
|
||||
}
|
||||
|
||||
// use the real global.setTimeout to schedule a callback to _runCallbacks.
|
||||
function _scheduleRealCallback() {
|
||||
if (_realCallbackKey) {
|
||||
global.clearTimeout(_realCallbackKey);
|
||||
// use the real global.setTimeout to schedule a callback to runCallbacks.
|
||||
function scheduleRealCallback(): void {
|
||||
if (realCallbackKey) {
|
||||
global.clearTimeout(realCallbackKey as NodeJS.Timeout);
|
||||
}
|
||||
|
||||
const first = _callbackList[0];
|
||||
const first = callbackList[0];
|
||||
|
||||
if (!first) {
|
||||
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
|
||||
debuglog("scheduleRealCallback: no more callbacks, not rescheduling");
|
||||
return;
|
||||
}
|
||||
|
||||
const now = _now();
|
||||
const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
|
||||
const timestamp = now();
|
||||
const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS);
|
||||
|
||||
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
|
||||
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
|
||||
debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs);
|
||||
realCallbackKey = global.setTimeout(runCallbacks, delayMs);
|
||||
}
|
||||
|
||||
function _runCallbacks() {
|
||||
function runCallbacks(): void {
|
||||
let cb;
|
||||
const now = _now();
|
||||
debuglog("_runCallbacks: now:", now);
|
||||
const timestamp = now();
|
||||
debuglog("runCallbacks: now:", timestamp);
|
||||
|
||||
// get the list of things to call
|
||||
const callbacksToRun = [];
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const first = _callbackList[0];
|
||||
if (!first || first.runAt > now) {
|
||||
const first = callbackList[0];
|
||||
if (!first || first.runAt > timestamp) {
|
||||
break;
|
||||
}
|
||||
cb = _callbackList.shift();
|
||||
debuglog("_runCallbacks: popping", cb.key);
|
||||
cb = callbackList.shift();
|
||||
debuglog("runCallbacks: popping", cb.key);
|
||||
callbacksToRun.push(cb);
|
||||
}
|
||||
|
||||
// reschedule the real callback before running our functions, to
|
||||
// keep the codepaths the same whether or not our functions
|
||||
// register their own setTimeouts.
|
||||
_scheduleRealCallback();
|
||||
scheduleRealCallback();
|
||||
|
||||
for (let i = 0; i < callbacksToRun.length; i++) {
|
||||
cb = callbacksToRun[i];
|
||||
@@ -172,7 +177,7 @@ function _runCallbacks() {
|
||||
cb.func.apply(global, cb.params);
|
||||
} catch (e) {
|
||||
logger.error("Uncaught exception in callback function",
|
||||
e.stack || e);
|
||||
e.stack || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +187,7 @@ function _runCallbacks() {
|
||||
* returns the index of the last element for which func returns
|
||||
* greater than zero, or array.length if no such element exists.
|
||||
*/
|
||||
function binarySearch(array, func) {
|
||||
function binarySearch<T>(array: T[], func: (v: T) => number): number {
|
||||
// min is inclusive, max exclusive.
|
||||
let min = 0;
|
||||
let max = array.length;
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module room-hierarchy
|
||||
*/
|
||||
|
||||
import { Room } from "./models/room";
|
||||
import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces";
|
||||
import { MatrixClient } from "./client";
|
||||
import { EventType } from "./@types/event";
|
||||
|
||||
export class RoomHierarchy {
|
||||
// Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy
|
||||
public readonly viaMap = new Map<string, Set<string>>();
|
||||
// Map from room id to list of rooms which claim this room as their child
|
||||
public readonly backRefs = new Map<string, string[]>();
|
||||
// Map from room id to object
|
||||
public readonly roomMap = new Map<string, IHierarchyRoom>();
|
||||
private loadRequest: ReturnType<MatrixClient["getRoomHierarchy"]>;
|
||||
private nextBatch?: string;
|
||||
private _rooms?: IHierarchyRoom[];
|
||||
private serverSupportError?: Error;
|
||||
|
||||
/**
|
||||
* Construct a new RoomHierarchy
|
||||
*
|
||||
* A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it.
|
||||
*
|
||||
* @param {Room} root the root of this hierarchy
|
||||
* @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request.
|
||||
* @param {number} maxDepth the maximum depth to traverse the hierarchy to
|
||||
* @param {boolean} suggestedOnly whether to only return rooms with suggested=true.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(
|
||||
private readonly root: Room,
|
||||
private readonly pageSize?: number,
|
||||
private readonly maxDepth?: number,
|
||||
private readonly suggestedOnly = false,
|
||||
) {}
|
||||
|
||||
public get noSupport(): boolean {
|
||||
return !!this.serverSupportError;
|
||||
}
|
||||
|
||||
public get canLoadMore(): boolean {
|
||||
return !!this.serverSupportError || !!this.nextBatch || !this._rooms;
|
||||
}
|
||||
|
||||
public get rooms(): IHierarchyRoom[] {
|
||||
return this._rooms;
|
||||
}
|
||||
|
||||
public async load(pageSize = this.pageSize): Promise<IHierarchyRoom[]> {
|
||||
if (this.loadRequest) return this.loadRequest.then(r => r.rooms);
|
||||
|
||||
this.loadRequest = this.root.client.getRoomHierarchy(
|
||||
this.root.roomId,
|
||||
pageSize,
|
||||
this.maxDepth,
|
||||
this.suggestedOnly,
|
||||
this.nextBatch,
|
||||
);
|
||||
|
||||
let rooms: IHierarchyRoom[];
|
||||
try {
|
||||
({ rooms, next_batch: this.nextBatch } = await this.loadRequest);
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_UNRECOGNIZED") {
|
||||
this.serverSupportError = e;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
} finally {
|
||||
this.loadRequest = null;
|
||||
}
|
||||
|
||||
if (this._rooms) {
|
||||
this._rooms = this._rooms.concat(rooms);
|
||||
} else {
|
||||
this._rooms = rooms;
|
||||
}
|
||||
|
||||
rooms.forEach(room => {
|
||||
this.roomMap.set(room.room_id, room);
|
||||
|
||||
room.children_state.forEach(ev => {
|
||||
if (ev.type !== EventType.SpaceChild) return;
|
||||
const childRoomId = ev.state_key;
|
||||
|
||||
// track backrefs for quicker hierarchy navigation
|
||||
if (!this.backRefs.has(childRoomId)) {
|
||||
this.backRefs.set(childRoomId, []);
|
||||
}
|
||||
this.backRefs.get(childRoomId).push(ev.room_id);
|
||||
|
||||
// fill viaMap
|
||||
if (Array.isArray(ev.content.via)) {
|
||||
if (!this.viaMap.has(childRoomId)) {
|
||||
this.viaMap.set(childRoomId, new Set());
|
||||
}
|
||||
const vias = this.viaMap.get(childRoomId);
|
||||
ev.content.via.forEach(via => vias.add(via));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
public getRelation(parentId: string, childId: string): IHierarchyRelation {
|
||||
return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId);
|
||||
}
|
||||
|
||||
public isSuggested(parentId: string, childId: string): boolean {
|
||||
return this.getRelation(parentId, childId)?.content.suggested;
|
||||
}
|
||||
|
||||
// locally remove a relation as a form of local echo
|
||||
public removeRelation(parentId: string, childId: string): void {
|
||||
const backRefs = this.backRefs.get(childId);
|
||||
if (backRefs?.length === 1) {
|
||||
this.backRefs.delete(childId);
|
||||
} else if (backRefs?.length) {
|
||||
this.backRefs.set(childId, backRefs.filter(ref => ref !== parentId));
|
||||
}
|
||||
|
||||
const room = this.roomMap.get(parentId);
|
||||
if (room) {
|
||||
room.children_state = room.children_state.filter(ev => ev.state_key !== childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is an internal module which manages queuing, scheduling and retrying
|
||||
* of requests.
|
||||
* @module scheduler
|
||||
*/
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
const DEBUG = false; // set true to enable console logging.
|
||||
|
||||
/**
|
||||
* Construct a scheduler for Matrix. Requires
|
||||
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
|
||||
* with a way of processing events.
|
||||
* @constructor
|
||||
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
|
||||
* algorithm to apply when determining when to try to send an event again.
|
||||
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
|
||||
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
|
||||
* algorithm to apply when determining which events should be sent before the
|
||||
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
|
||||
*/
|
||||
export function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
|
||||
this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT;
|
||||
this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES;
|
||||
this._queues = {
|
||||
// queueName: [{
|
||||
// event: MatrixEvent, // event to send
|
||||
// defer: Deferred, // defer to resolve/reject at the END of the retries
|
||||
// attempts: Number // number of times we've called processFn
|
||||
// }, ...]
|
||||
};
|
||||
this._activeQueues = [];
|
||||
this._procFn = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a queue based on an event. The event provided does not need to be in
|
||||
* the queue.
|
||||
* @param {MatrixEvent} event An event to get the queue for.
|
||||
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
|
||||
* Modifying this array will not modify the list itself. Modifying events in
|
||||
* this array <i>will</i> modify the underlying event in the queue.
|
||||
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
|
||||
*/
|
||||
MatrixScheduler.prototype.getQueueForEvent = function(event) {
|
||||
const name = this.queueAlgorithm(event);
|
||||
if (!name || !this._queues[name]) {
|
||||
return null;
|
||||
}
|
||||
return this._queues[name].map(function(obj) {
|
||||
return obj.event;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove this event from the queue. The event is equal to another event if they
|
||||
* have the same ID returned from event.getId().
|
||||
* @param {MatrixEvent} event The event to remove.
|
||||
* @return {boolean} True if this event was removed.
|
||||
*/
|
||||
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
|
||||
const name = this.queueAlgorithm(event);
|
||||
if (!name || !this._queues[name]) {
|
||||
return false;
|
||||
}
|
||||
let removed = false;
|
||||
utils.removeElement(this._queues[name], function(element) {
|
||||
if (element.event.getId() === event.getId()) {
|
||||
// XXX we should probably reject the promise?
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
removed = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return removed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the process function. Required for events in the queue to be processed.
|
||||
* If set after events have been added to the queue, this will immediately start
|
||||
* processing them.
|
||||
* @param {module:scheduler~processFn} fn The function that can process events
|
||||
* in the queue.
|
||||
*/
|
||||
MatrixScheduler.prototype.setProcessFunction = function(fn) {
|
||||
this._procFn = fn;
|
||||
_startProcessingQueues(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Queue an event if it is required and start processing queues.
|
||||
* @param {MatrixEvent} event The event that may be queued.
|
||||
* @return {?Promise} A promise if the event was queued, which will be
|
||||
* resolved or rejected in due time, else null.
|
||||
*/
|
||||
MatrixScheduler.prototype.queueEvent = function(event) {
|
||||
const queueName = this.queueAlgorithm(event);
|
||||
if (!queueName) {
|
||||
return null;
|
||||
}
|
||||
// add the event to the queue and make a deferred for it.
|
||||
if (!this._queues[queueName]) {
|
||||
this._queues[queueName] = [];
|
||||
}
|
||||
const defer = utils.defer();
|
||||
this._queues[queueName].push({
|
||||
event: event,
|
||||
defer: defer,
|
||||
attempts: 0,
|
||||
});
|
||||
debuglog(
|
||||
"Queue algorithm dumped event %s into queue '%s'",
|
||||
event.getId(), queueName,
|
||||
);
|
||||
_startProcessingQueues(this);
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retries events up to 4 times using exponential backoff. This produces wait
|
||||
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is
|
||||
* waited before being retried.
|
||||
* @param {MatrixEvent} event
|
||||
* @param {Number} attempts
|
||||
* @param {MatrixError} err
|
||||
* @return {Number}
|
||||
* @see module:scheduler~retryAlgorithm
|
||||
*/
|
||||
MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// 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") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if event that we are trying to send is too large in any way then retrying won't help
|
||||
if (err.name === "M_TOO_LARGE") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
return (1000 * Math.pow(2, attempts));
|
||||
};
|
||||
|
||||
/**
|
||||
* Queues <code>m.room.message</code> events and lets other events continue
|
||||
* concurrently.
|
||||
* @param {MatrixEvent} event
|
||||
* @return {string}
|
||||
* @see module:scheduler~queueAlgorithm
|
||||
*/
|
||||
MatrixScheduler.QUEUE_MESSAGES = function(event) {
|
||||
// enqueue messages or events that associate with another event (redactions and relations)
|
||||
if (event.getType() === "m.room.message" || event.hasAssocation()) {
|
||||
// put these events in the 'message' queue.
|
||||
return "message";
|
||||
}
|
||||
// allow all other events continue concurrently.
|
||||
return null;
|
||||
};
|
||||
|
||||
function _startProcessingQueues(scheduler) {
|
||||
if (!scheduler._procFn) {
|
||||
return;
|
||||
}
|
||||
// for each inactive queue with events in them
|
||||
Object.keys(scheduler._queues)
|
||||
.filter(function(queueName) {
|
||||
return scheduler._activeQueues.indexOf(queueName) === -1 &&
|
||||
scheduler._queues[queueName].length > 0;
|
||||
})
|
||||
.forEach(function(queueName) {
|
||||
// mark the queue as active
|
||||
scheduler._activeQueues.push(queueName);
|
||||
// begin processing the head of the queue
|
||||
debuglog("Spinning up queue: '%s'", queueName);
|
||||
_processQueue(scheduler, queueName);
|
||||
});
|
||||
}
|
||||
|
||||
function _processQueue(scheduler, queueName) {
|
||||
// get head of queue
|
||||
const obj = _peekNextEvent(scheduler, queueName);
|
||||
if (!obj) {
|
||||
// queue is empty. Mark as inactive and stop recursing.
|
||||
const index = scheduler._activeQueues.indexOf(queueName);
|
||||
if (index >= 0) {
|
||||
scheduler._activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
return;
|
||||
}
|
||||
debuglog(
|
||||
"Queue '%s' has %s pending events",
|
||||
queueName, scheduler._queues[queueName].length,
|
||||
);
|
||||
// fire the process function and if it resolves, resolve the deferred. Else
|
||||
// invoke the retry algorithm.
|
||||
|
||||
// First wait for a resolved promise, so the resolve handlers for
|
||||
// the deferred of the previously sent event can run.
|
||||
// This way enqueued relations/redactions to enqueued events can receive
|
||||
// the remove id of their target before being sent.
|
||||
Promise.resolve().then(() => {
|
||||
return scheduler._procFn(obj.event);
|
||||
}).then(function(res) {
|
||||
// remove this from the queue
|
||||
_removeNextEvent(scheduler, queueName);
|
||||
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
|
||||
obj.defer.resolve(res);
|
||||
// keep processing
|
||||
_processQueue(scheduler, queueName);
|
||||
}, function(err) {
|
||||
obj.attempts += 1;
|
||||
// ask the retry algorithm when/if we should try again
|
||||
const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
|
||||
debuglog(
|
||||
"retry(%s) err=%s event_id=%s waitTime=%s",
|
||||
obj.attempts, err, obj.event.getId(), waitTimeMs,
|
||||
);
|
||||
if (waitTimeMs === -1) { // give up (you quitter!)
|
||||
debuglog(
|
||||
"Queue '%s' giving up on event %s", queueName, obj.event.getId(),
|
||||
);
|
||||
// remove this from the queue
|
||||
_removeNextEvent(scheduler, queueName);
|
||||
obj.defer.reject(err);
|
||||
// process next event
|
||||
_processQueue(scheduler, queueName);
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
_processQueue(scheduler, queueName);
|
||||
}, waitTimeMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _peekNextEvent(scheduler, queueName) {
|
||||
const queue = scheduler._queues[queueName];
|
||||
if (!Array.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue[0];
|
||||
}
|
||||
|
||||
function _removeNextEvent(scheduler, queueName) {
|
||||
const queue = scheduler._queues[queueName];
|
||||
if (!Array.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue.shift();
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
logger.log(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry algorithm to apply when retrying events. To stop retrying, return
|
||||
* <code>-1</code>. If this event was part of a queue, it will be removed from
|
||||
* the queue.
|
||||
* @callback retryAlgorithm
|
||||
* @param {MatrixEvent} event The event being retried.
|
||||
* @param {Number} attempts The number of failed attempts. This will always be
|
||||
* >= 1.
|
||||
* @param {MatrixError} err The most recent error message received when trying
|
||||
* to send this event.
|
||||
* @return {Number} The number of milliseconds to wait before trying again. If
|
||||
* this is 0, the request will be immediately retried. If this is
|
||||
* <code>-1</code>, the event will be marked as
|
||||
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The queuing algorithm to apply to events. This function must be idempotent as
|
||||
* it may be called multiple times with the same event. All queues created are
|
||||
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
|
||||
* which will not put this event in a queue. Events that fail to send that form
|
||||
* part of a queue will be removed from the queue and the next event in the
|
||||
* queue will be sent.
|
||||
* @callback queueAlgorithm
|
||||
* @param {MatrixEvent} event The event to be sent.
|
||||
* @return {string} The name of the queue to put the event into. If a queue with
|
||||
* this name does not exist, it will be created. If this is <code>null</code>,
|
||||
* the event is not put into a queue and will be sent concurrently.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to invoke to process (send) events in the queue.
|
||||
* @callback processFn
|
||||
* @param {MatrixEvent} event The event to send.
|
||||
* @return {Promise} Resolved/rejected depending on the outcome of the request.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
Copyright 2015 - 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is an internal module which manages queuing, scheduling and retrying
|
||||
* of requests.
|
||||
* @module scheduler
|
||||
*/
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
|
||||
const DEBUG = false; // set true to enable console logging.
|
||||
|
||||
interface IQueueEntry<T> {
|
||||
event: MatrixEvent;
|
||||
defer: IDeferred<T>;
|
||||
attempts: number;
|
||||
}
|
||||
|
||||
type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
|
||||
|
||||
/**
|
||||
* Construct a scheduler for Matrix. Requires
|
||||
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
|
||||
* with a way of processing events.
|
||||
* @constructor
|
||||
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
|
||||
* algorithm to apply when determining when to try to send an event again.
|
||||
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
|
||||
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
|
||||
* algorithm to apply when determining which events should be sent before the
|
||||
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
export class MatrixScheduler<T = ISendEventResponse> {
|
||||
/**
|
||||
* Retries events up to 4 times using exponential backoff. This produces wait
|
||||
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is
|
||||
* waited before being retried.
|
||||
* @param {MatrixEvent} event
|
||||
* @param {Number} attempts
|
||||
* @param {MatrixError} err
|
||||
* @return {Number}
|
||||
* @see module:scheduler~retryAlgorithm
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number {
|
||||
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// 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") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if event that we are trying to send is too large in any way then retrying won't help
|
||||
if (err.name === "M_TOO_LARGE") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
return (1000 * Math.pow(2, attempts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues <code>m.room.message</code> events and lets other events continue
|
||||
* concurrently.
|
||||
* @param {MatrixEvent} event
|
||||
* @return {string}
|
||||
* @see module:scheduler~queueAlgorithm
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static QUEUE_MESSAGES(event: MatrixEvent) {
|
||||
// enqueue messages or events that associate with another event (redactions and relations)
|
||||
if (event.getType() === EventType.RoomMessage || event.hasAssocation()) {
|
||||
// put these events in the 'message' queue.
|
||||
return "message";
|
||||
}
|
||||
// allow all other events continue concurrently.
|
||||
return null;
|
||||
}
|
||||
|
||||
// queueName: [{
|
||||
// event: MatrixEvent, // event to send
|
||||
// defer: Deferred, // defer to resolve/reject at the END of the retries
|
||||
// attempts: Number // number of times we've called processFn
|
||||
// }, ...]
|
||||
private readonly queues: Record<string, IQueueEntry<T>[]> = {};
|
||||
private activeQueues: string[] = [];
|
||||
private procFn: ProcessFunction<T> = null;
|
||||
|
||||
constructor(
|
||||
public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
|
||||
public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve a queue based on an event. The event provided does not need to be in
|
||||
* the queue.
|
||||
* @param {MatrixEvent} event An event to get the queue for.
|
||||
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
|
||||
* Modifying this array will not modify the list itself. Modifying events in
|
||||
* this array <i>will</i> modify the underlying event in the queue.
|
||||
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
|
||||
*/
|
||||
public getQueueForEvent(event: MatrixEvent): MatrixEvent[] {
|
||||
const name = this.queueAlgorithm(event);
|
||||
if (!name || !this.queues[name]) {
|
||||
return null;
|
||||
}
|
||||
return this.queues[name].map(function(obj) {
|
||||
return obj.event;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this event from the queue. The event is equal to another event if they
|
||||
* have the same ID returned from event.getId().
|
||||
* @param {MatrixEvent} event The event to remove.
|
||||
* @return {boolean} True if this event was removed.
|
||||
*/
|
||||
public removeEventFromQueue(event: MatrixEvent): boolean {
|
||||
const name = this.queueAlgorithm(event);
|
||||
if (!name || !this.queues[name]) {
|
||||
return false;
|
||||
}
|
||||
let removed = false;
|
||||
utils.removeElement(this.queues[name], (element) => {
|
||||
if (element.event.getId() === event.getId()) {
|
||||
// XXX we should probably reject the promise?
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
removed = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the process function. Required for events in the queue to be processed.
|
||||
* If set after events have been added to the queue, this will immediately start
|
||||
* processing them.
|
||||
* @param {module:scheduler~processFn} fn The function that can process events
|
||||
* in the queue.
|
||||
*/
|
||||
public setProcessFunction(fn: ProcessFunction<T>): void {
|
||||
this.procFn = fn;
|
||||
this.startProcessingQueues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an event if it is required and start processing queues.
|
||||
* @param {MatrixEvent} event The event that may be queued.
|
||||
* @return {?Promise} A promise if the event was queued, which will be
|
||||
* resolved or rejected in due time, else null.
|
||||
*/
|
||||
public queueEvent(event: MatrixEvent): Promise<T> | null {
|
||||
const queueName = this.queueAlgorithm(event);
|
||||
if (!queueName) {
|
||||
return null;
|
||||
}
|
||||
// add the event to the queue and make a deferred for it.
|
||||
if (!this.queues[queueName]) {
|
||||
this.queues[queueName] = [];
|
||||
}
|
||||
const defer = utils.defer<T>();
|
||||
this.queues[queueName].push({
|
||||
event: event,
|
||||
defer: defer,
|
||||
attempts: 0,
|
||||
});
|
||||
debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
|
||||
this.startProcessingQueues();
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
private startProcessingQueues(): void {
|
||||
if (!this.procFn) return;
|
||||
// for each inactive queue with events in them
|
||||
Object.keys(this.queues)
|
||||
.filter((queueName) => {
|
||||
return this.activeQueues.indexOf(queueName) === -1 &&
|
||||
this.queues[queueName].length > 0;
|
||||
})
|
||||
.forEach((queueName) => {
|
||||
// mark the queue as active
|
||||
this.activeQueues.push(queueName);
|
||||
// begin processing the head of the queue
|
||||
debuglog("Spinning up queue: '%s'", queueName);
|
||||
this.processQueue(queueName);
|
||||
});
|
||||
}
|
||||
|
||||
private processQueue = (queueName: string): void => {
|
||||
// get head of queue
|
||||
const obj = this.peekNextEvent(queueName);
|
||||
if (!obj) {
|
||||
// queue is empty. Mark as inactive and stop recursing.
|
||||
const index = this.activeQueues.indexOf(queueName);
|
||||
if (index >= 0) {
|
||||
this.activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
return;
|
||||
}
|
||||
debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length);
|
||||
// fire the process function and if it resolves, resolve the deferred. Else
|
||||
// invoke the retry algorithm.
|
||||
|
||||
// First wait for a resolved promise, so the resolve handlers for
|
||||
// the deferred of the previously sent event can run.
|
||||
// This way enqueued relations/redactions to enqueued events can receive
|
||||
// the remove id of their target before being sent.
|
||||
Promise.resolve().then(() => {
|
||||
return this.procFn(obj.event);
|
||||
}).then((res) => {
|
||||
// remove this from the queue
|
||||
this.removeNextEvent(queueName);
|
||||
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
|
||||
obj.defer.resolve(res);
|
||||
// keep processing
|
||||
this.processQueue(queueName);
|
||||
}, (err) => {
|
||||
obj.attempts += 1;
|
||||
// ask the retry algorithm when/if we should try again
|
||||
const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err);
|
||||
debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs);
|
||||
if (waitTimeMs === -1) { // give up (you quitter!)
|
||||
debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
|
||||
// remove this from the queue
|
||||
this.removeNextEvent(queueName);
|
||||
obj.defer.reject(err);
|
||||
// process next event
|
||||
this.processQueue(queueName);
|
||||
} else {
|
||||
setTimeout(this.processQueue, waitTimeMs, queueName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private peekNextEvent(queueName: string): IQueueEntry<T> {
|
||||
const queue = this.queues[queueName];
|
||||
if (!Array.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue[0];
|
||||
}
|
||||
|
||||
private removeNextEvent(queueName: string): IQueueEntry<T> {
|
||||
const queue = this.queues[queueName];
|
||||
if (!Array.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
function debuglog(...args) {
|
||||
if (DEBUG) {
|
||||
logger.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry algorithm to apply when retrying events. To stop retrying, return
|
||||
* <code>-1</code>. If this event was part of a queue, it will be removed from
|
||||
* the queue.
|
||||
* @callback retryAlgorithm
|
||||
* @param {MatrixEvent} event The event being retried.
|
||||
* @param {Number} attempts The number of failed attempts. This will always be
|
||||
* >= 1.
|
||||
* @param {MatrixError} err The most recent error message received when trying
|
||||
* to send this event.
|
||||
* @return {Number} The number of milliseconds to wait before trying again. If
|
||||
* this is 0, the request will be immediately retried. If this is
|
||||
* <code>-1</code>, the event will be marked as
|
||||
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The queuing algorithm to apply to events. This function must be idempotent as
|
||||
* it may be called multiple times with the same event. All queues created are
|
||||
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
|
||||
* which will not put this event in a queue. Events that fail to send that form
|
||||
* part of a queue will be removed from the queue and the next event in the
|
||||
* queue will be sent.
|
||||
* @callback queueAlgorithm
|
||||
* @param {MatrixEvent} event The event to be sent.
|
||||
* @return {string} The name of the queue to put the event into. If a queue with
|
||||
* this name does not exist, it will be created. If this is <code>null</code>,
|
||||
* the event is not put into a queue and will be sent concurrently.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to invoke to process (send) events in the queue.
|
||||
* @callback processFn
|
||||
* @param {MatrixEvent} event The event to send.
|
||||
* @return {Promise} Resolved/rejected depending on the outcome of the request.
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface IStore {
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Group} group
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
storeGroup(group: Group);
|
||||
|
||||
@@ -63,12 +64,14 @@ export interface IStore {
|
||||
* No-op.
|
||||
* @param {string} groupId
|
||||
* @return {null}
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
getGroup(groupId: string): Group | null;
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
getGroups(): Group[];
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ export class MemoryStore implements IStore {
|
||||
/**
|
||||
* Store the given room.
|
||||
* @param {Group} group The group to be stored
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public storeGroup(group: Group) {
|
||||
this.groups[group.groupId] = group;
|
||||
@@ -102,6 +103,7 @@ export class MemoryStore implements IStore {
|
||||
* Retrieve a group by its group ID.
|
||||
* @param {string} groupId The group ID.
|
||||
* @return {Group} The group or null.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroup(groupId: string): Group | null {
|
||||
return this.groups[groupId] || null;
|
||||
@@ -110,6 +112,7 @@ export class MemoryStore implements IStore {
|
||||
/**
|
||||
* Retrieve all known groups.
|
||||
* @return {Group[]} A list of groups, which may be empty.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroups(): Group[] {
|
||||
return Object.values(this.groups);
|
||||
|
||||
@@ -61,6 +61,7 @@ export class StubStore implements IStore {
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Group} group
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public storeGroup(group: Group) {}
|
||||
|
||||
@@ -68,6 +69,7 @@ export class StubStore implements IStore {
|
||||
* No-op.
|
||||
* @param {string} groupId
|
||||
* @return {null}
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroup(groupId: string): Group | null {
|
||||
return null;
|
||||
@@ -76,6 +78,7 @@ export class StubStore implements IStore {
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
public getGroups(): Group[] {
|
||||
return [];
|
||||
|
||||
+90
-13
@@ -52,6 +52,8 @@ import {
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { ISavedSync } from "./store";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IPushRules } from "./@types/PushRules";
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
@@ -146,6 +148,7 @@ export class SyncApi {
|
||||
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
|
||||
this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000);
|
||||
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
|
||||
|
||||
if (!opts.canResetEntireTimeline) {
|
||||
opts.canResetEntireTimeline = (roomId: string) => {
|
||||
@@ -281,8 +284,9 @@ export class SyncApi {
|
||||
return;
|
||||
}
|
||||
leaveObj.timeline = leaveObj.timeline || {};
|
||||
const timelineEvents =
|
||||
this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
@@ -291,17 +295,51 @@ export class SyncApi {
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
this.processEventsForNotifs(room, events);
|
||||
});
|
||||
return rooms;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getWireContent()["m.relates_to"]?.event_id;
|
||||
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.
|
||||
@@ -678,7 +716,11 @@ export class SyncApi {
|
||||
*/
|
||||
public stop(): void {
|
||||
debuglog("SyncApi.stop");
|
||||
if (global.window) {
|
||||
// It is necessary to check for the existance of
|
||||
// global.window AND global.window.removeEventListener.
|
||||
// Some platforms (e.g. React Native) register global.window,
|
||||
// but do not have global.window.removeEventListener.
|
||||
if (global.window && global.window.removeEventListener) {
|
||||
global.window.removeEventListener("online", this.onOnline, false);
|
||||
}
|
||||
this.running = false;
|
||||
@@ -1065,8 +1107,8 @@ export class SyncApi {
|
||||
// honour push rules that were previously cached. Base rules
|
||||
// will be updated when we receive push rules via getPushRules
|
||||
// (see sync) before syncing over the network.
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
const rules = accountDataEvent.getContent();
|
||||
if (accountDataEvent.getType() === EventType.PushRules) {
|
||||
const rules = accountDataEvent.getContent<IPushRules>();
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getId()];
|
||||
@@ -1191,7 +1233,7 @@ export class SyncApi {
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const events = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
|
||||
@@ -1238,8 +1280,8 @@ export class SyncApi {
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
const eventId = timelineEvents[i].getId();
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const eventId = events[i].getId();
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
debuglog("Already have event " + eventId + " in limited " +
|
||||
"sync - not resetting");
|
||||
@@ -1248,7 +1290,7 @@ export class SyncApi {
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
events.splice(0, i);
|
||||
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
@@ -1278,7 +1320,10 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1299,7 +1344,7 @@ export class SyncApi {
|
||||
client.emit("Room", room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
const processRoomEvent = async (e) => {
|
||||
client.emit("event", e);
|
||||
@@ -1320,6 +1365,7 @@ export class SyncApi {
|
||||
|
||||
await utils.promiseMapSeries(stateEvents, processRoomEvent);
|
||||
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
|
||||
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
|
||||
ephemeralEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
@@ -1339,10 +1385,13 @@ export class SyncApi {
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
const timelineEvents = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
@@ -1351,7 +1400,7 @@ export class SyncApi {
|
||||
client.emit("Room", room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
@@ -1359,6 +1408,9 @@ export class SyncApi {
|
||||
timelineEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
threadedEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
@@ -1678,6 +1730,31 @@ export class SyncApi {
|
||||
room.addLiveEvents(timelineEventList || [], null, fromCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
threadedEvents
|
||||
.sort((a, b) => a.getTs() - b.getTs())
|
||||
.forEach(event => {
|
||||
room.addThreadedEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
|
||||
// relatedEvents.push(event);
|
||||
|
||||
// const parentEventId = event.parentEventId;
|
||||
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
|
||||
|
||||
// if (parentEventIndex > -1) {
|
||||
// const [relatedEvent] = events.splice(parentEventIndex, 1);
|
||||
// return this.extractRelatedEvents(relatedEvent, events, relatedEvents);
|
||||
// } else {
|
||||
// return relatedEvents;
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Takes a list of timelineEvents and adds and adds to notifEvents
|
||||
* as appropriate.
|
||||
|
||||
+2
-2
@@ -412,7 +412,7 @@ export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function globToRegexp(glob: string, extended: any): string {
|
||||
export function globToRegexp(glob: string, extended?: any): string {
|
||||
extended = typeof(extended) === 'boolean' ? extended : true;
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
@@ -457,7 +457,7 @@ export interface IDeferred<T> {
|
||||
}
|
||||
|
||||
// Returns a Deferred
|
||||
export function defer<T>(): IDeferred<T> {
|
||||
export function defer<T = void>(): IDeferred<T> {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
|
||||
+212
-274
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import { logger } from '../logger';
|
||||
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
||||
import { EventType } from '../@types/event';
|
||||
import { MatrixClient } from '../client';
|
||||
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
|
||||
|
||||
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
|
||||
// time to press the 'accept' button
|
||||
@@ -222,6 +223,7 @@ export class CallEventHandler {
|
||||
} else {
|
||||
this.client.emit("Call.incoming", call);
|
||||
}
|
||||
return;
|
||||
} else if (type === EventType.CallCandidates) {
|
||||
if (weSentTheEvent) return;
|
||||
|
||||
@@ -234,6 +236,7 @@ export class CallEventHandler {
|
||||
} else {
|
||||
call.onRemoteIceCandidatesReceived(event);
|
||||
}
|
||||
return;
|
||||
} else if ([EventType.CallHangup, EventType.CallReject].includes(type)) {
|
||||
// Note that we also observe our own hangups here so we can see
|
||||
// if we've already rejected a call that would otherwise be valid
|
||||
@@ -250,17 +253,21 @@ export class CallEventHandler {
|
||||
} else {
|
||||
if (call.state !== CallState.Ended) {
|
||||
if (type === EventType.CallHangup) {
|
||||
call.onHangupReceived(content);
|
||||
call.onHangupReceived(content as MCallHangupReject);
|
||||
} else {
|
||||
call.onRejectReceived(content);
|
||||
call.onRejectReceived(content as MCallHangupReject);
|
||||
}
|
||||
this.calls.delete(content.call_id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The following events need a call
|
||||
if (!call) return;
|
||||
// The following events need a call and a peer connection
|
||||
if (!call || !call.hasPeerConnection) {
|
||||
logger.warn("Discarding an event, we don't have a call/peerConn", type);
|
||||
return;
|
||||
}
|
||||
// Ignore remote echo
|
||||
if (event.getContent().party_id === call.ourPartyId) return;
|
||||
|
||||
@@ -268,7 +275,7 @@ export class CallEventHandler {
|
||||
case EventType.CallAnswer:
|
||||
if (weSentTheEvent) {
|
||||
if (call.state === CallState.Ringing) {
|
||||
call.onAnsweredElsewhere(content);
|
||||
call.onAnsweredElsewhere(content as MCallAnswer);
|
||||
}
|
||||
} else {
|
||||
call.onAnswerReceived(event);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// allow non-camelcase as these are events type that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { CallErrorCode } from "./call";
|
||||
|
||||
// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
|
||||
export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
|
||||
|
||||
@@ -19,11 +21,6 @@ export interface SDPStreamMetadata {
|
||||
[key: string]: SDPStreamMetadataObject;
|
||||
}
|
||||
|
||||
interface CallOfferAnswer {
|
||||
type: string;
|
||||
sdp: string;
|
||||
}
|
||||
|
||||
export interface CallCapabilities {
|
||||
'm.call.transferee': boolean;
|
||||
'm.call.dtmf': boolean;
|
||||
@@ -35,29 +32,56 @@ export interface CallReplacesTarget {
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export interface MCallAnswer {
|
||||
answer: CallOfferAnswer;
|
||||
capabilities: CallCapabilities;
|
||||
export interface MCallBase {
|
||||
call_id: string;
|
||||
version: string | number;
|
||||
party_id?: string;
|
||||
}
|
||||
|
||||
export interface MCallAnswer extends MCallBase {
|
||||
answer: RTCSessionDescription;
|
||||
capabilities?: CallCapabilities;
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallOfferNegotiate {
|
||||
offer: CallOfferAnswer;
|
||||
description: CallOfferAnswer;
|
||||
export interface MCallSelectAnswer extends MCallBase {
|
||||
selected_party_id: string;
|
||||
}
|
||||
|
||||
export interface MCallInviteNegotiate extends MCallBase {
|
||||
offer: RTCSessionDescription;
|
||||
description: RTCSessionDescription;
|
||||
lifetime: number;
|
||||
capabilities: CallCapabilities;
|
||||
capabilities?: CallCapabilities;
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallSDPStreamMetadataChanged {
|
||||
export interface MCallSDPStreamMetadataChanged extends MCallBase {
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallReplacesEvent {
|
||||
export interface MCallReplacesEvent extends MCallBase {
|
||||
replacement_id: string;
|
||||
target_user: CallReplacesTarget;
|
||||
create_call: string;
|
||||
await_call: string;
|
||||
target_room: string;
|
||||
}
|
||||
|
||||
export interface MCAllAssertedIdentity extends MCallBase {
|
||||
asserted_identity: {
|
||||
id: string;
|
||||
display_name: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MCallCandidates extends MCallBase {
|
||||
candidates: RTCIceCandidate[];
|
||||
}
|
||||
|
||||
export interface MCallHangupReject extends MCallBase {
|
||||
reason?: CallErrorCode;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
+103
-2
@@ -19,12 +19,25 @@ import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { MatrixClient } from "../client";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
|
||||
const POLLING_INTERVAL = 250; // ms
|
||||
const SPEAKING_THRESHOLD = -60; // dB
|
||||
|
||||
export enum CallFeedEvent {
|
||||
NewStream = "new_stream",
|
||||
MuteStateChanged = "mute_state_changed"
|
||||
MuteStateChanged = "mute_state_changed",
|
||||
VolumeChanged = "volume_changed",
|
||||
Speaking = "speaking",
|
||||
}
|
||||
|
||||
export class CallFeed extends EventEmitter {
|
||||
private measuringVolumeActivity = false;
|
||||
private audioContext: AudioContext;
|
||||
private analyser: AnalyserNode;
|
||||
private frequencyBinCount: Float32Array;
|
||||
private speakingThreshold = SPEAKING_THRESHOLD;
|
||||
private speaking = false;
|
||||
private volumeLooperTimeout: number;
|
||||
|
||||
constructor(
|
||||
public stream: MediaStream,
|
||||
public userId: string,
|
||||
@@ -35,6 +48,30 @@ export class CallFeed extends EventEmitter {
|
||||
private videoMuted: boolean,
|
||||
) {
|
||||
super();
|
||||
|
||||
if (this.hasAudioTrack) {
|
||||
this.initVolumeMeasuring();
|
||||
}
|
||||
}
|
||||
|
||||
private get hasAudioTrack(): boolean {
|
||||
return this.stream.getAudioTracks().length > 0;
|
||||
}
|
||||
|
||||
private initVolumeMeasuring(): void {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (!this.hasAudioTrack || !AudioContext) return;
|
||||
|
||||
this.audioContext = new AudioContext();
|
||||
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 512;
|
||||
this.analyser.smoothingTimeConstant = 0.1;
|
||||
|
||||
const mediaStreamAudioSourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
||||
mediaStreamAudioSourceNode.connect(this.analyser);
|
||||
|
||||
this.frequencyBinCount = new Float32Array(this.analyser.frequencyBinCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,18 +115,82 @@ export class CallFeed extends EventEmitter {
|
||||
* This method should be only used by MatrixCall.
|
||||
* @param newStream new stream with which to replace the current one
|
||||
*/
|
||||
public setNewStream(newStream: MediaStream) {
|
||||
public setNewStream(newStream: MediaStream): void {
|
||||
this.stream = newStream;
|
||||
this.emit(CallFeedEvent.NewStream, this.stream);
|
||||
|
||||
if (this.hasAudioTrack) {
|
||||
this.initVolumeMeasuring();
|
||||
} else {
|
||||
this.measureVolumeActivity(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed's internal audio mute state
|
||||
* @param muted is the feed's audio muted?
|
||||
*/
|
||||
public setAudioMuted(muted: boolean): void {
|
||||
this.audioMuted = muted;
|
||||
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed's internal video mute state
|
||||
* @param muted is the feed's video muted?
|
||||
*/
|
||||
public setVideoMuted(muted: boolean): void {
|
||||
this.videoMuted = muted;
|
||||
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts emitting volume_changed events where the emitter value is in decibels
|
||||
* @param enabled emit volume changes
|
||||
*/
|
||||
public measureVolumeActivity(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
if (!this.audioContext || !this.analyser || !this.frequencyBinCount || !this.hasAudioTrack) return;
|
||||
|
||||
this.measuringVolumeActivity = true;
|
||||
this.volumeLooper();
|
||||
} else {
|
||||
this.measuringVolumeActivity = false;
|
||||
this.emit(CallFeedEvent.VolumeChanged, -Infinity);
|
||||
}
|
||||
}
|
||||
|
||||
public setSpeakingThreshold(threshold: number) {
|
||||
this.speakingThreshold = threshold;
|
||||
}
|
||||
|
||||
private volumeLooper(): void {
|
||||
if (!this.analyser) return;
|
||||
|
||||
this.volumeLooperTimeout = setTimeout(() => {
|
||||
if (!this.measuringVolumeActivity) return;
|
||||
|
||||
this.analyser.getFloatFrequencyData(this.frequencyBinCount);
|
||||
|
||||
let maxVolume = -Infinity;
|
||||
for (let i = 0; i < this.frequencyBinCount.length; i++) {
|
||||
if (this.frequencyBinCount[i] > maxVolume) {
|
||||
maxVolume = this.frequencyBinCount[i];
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(CallFeedEvent.VolumeChanged, maxVolume);
|
||||
const newSpeaking = maxVolume > this.speakingThreshold;
|
||||
if (this.speaking !== newSpeaking) {
|
||||
this.speaking = newSpeaking;
|
||||
this.emit(CallFeedEvent.Speaking, this.speaking);
|
||||
}
|
||||
|
||||
this.volumeLooper();
|
||||
}, POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
clearTimeout(this.volumeLooperTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class MediaHandler {
|
||||
private audioInput: string;
|
||||
private videoInput: string;
|
||||
private userMediaStreams: MediaStream[] = [];
|
||||
private screensharingStreams: MediaStream[] = [];
|
||||
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setAudioInput(deviceId: string): void {
|
||||
this.audioInput = deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setVideoInput(deviceId: string): void {
|
||||
this.videoInput = deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
|
||||
let stream: MediaStream;
|
||||
|
||||
// Find a stream with matching tracks
|
||||
const matchingStream = this.userMediaStreams.find((stream) => {
|
||||
if (audio !== (stream.getAudioTracks().length > 0)) return false;
|
||||
if (video !== (stream.getVideoTracks().length > 0)) return false;
|
||||
});
|
||||
|
||||
if (matchingStream) {
|
||||
logger.log("Cloning user media stream", matchingStream.id);
|
||||
stream = matchingStream.clone();
|
||||
} else {
|
||||
const constraints = this.getUserMediaContraints(audio, video);
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
|
||||
this.userMediaStreams.push(stream);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all tracks on the provided usermedia stream
|
||||
*/
|
||||
public stopUserMediaStream(mediaStream: MediaStream) {
|
||||
logger.debug("Stopping usermedia stream", mediaStream.id);
|
||||
for (const track of mediaStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
|
||||
const index = this.userMediaStreams.indexOf(mediaStream);
|
||||
|
||||
if (index !== -1) {
|
||||
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
|
||||
this.userMediaStreams.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream | null> {
|
||||
let stream: MediaStream;
|
||||
|
||||
if (this.screensharingStreams.length === 0) {
|
||||
const screenshareConstraints = this.getScreenshareContraints(desktopCapturerSourceId);
|
||||
if (!screenshareConstraints) return null;
|
||||
|
||||
if (desktopCapturerSourceId) {
|
||||
// We are using Electron
|
||||
logger.debug("Getting screensharing stream using getUserMedia()", desktopCapturerSourceId);
|
||||
stream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
|
||||
} else {
|
||||
// We are not using Electron
|
||||
logger.debug("Getting screensharing stream using getDisplayMedia()");
|
||||
stream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
|
||||
}
|
||||
} else {
|
||||
const matchingStream = this.screensharingStreams[this.screensharingStreams.length - 1];
|
||||
logger.log("Cloning screensharing stream", matchingStream.id);
|
||||
stream = matchingStream.clone();
|
||||
}
|
||||
|
||||
this.screensharingStreams.push(stream);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all tracks on the provided screensharing stream
|
||||
*/
|
||||
public stopScreensharingStream(mediaStream: MediaStream) {
|
||||
logger.debug("Stopping screensharing stream", mediaStream.id);
|
||||
for (const track of mediaStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
|
||||
const index = this.screensharingStreams.indexOf(mediaStream);
|
||||
|
||||
if (index !== -1) {
|
||||
logger.debug("Splicing screensharing stream out stream array", mediaStream.id);
|
||||
this.screensharingStreams.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all local media tracks
|
||||
*/
|
||||
public stopAllStreams() {
|
||||
for (const stream of this.userMediaStreams) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
for (const stream of this.screensharingStreams) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
this.userMediaStreams = [];
|
||||
this.screensharingStreams = [];
|
||||
}
|
||||
|
||||
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
|
||||
const isWebkit = !!navigator.webkitGetUserMedia;
|
||||
|
||||
return {
|
||||
audio: audio
|
||||
? {
|
||||
deviceId: this.audioInput ? { ideal: this.audioInput } : undefined,
|
||||
}
|
||||
: false,
|
||||
video: video
|
||||
? {
|
||||
deviceId: this.videoInput ? { ideal: this.videoInput } : undefined,
|
||||
/* We want 640x360. Chrome will give it only if we ask exactly,
|
||||
FF refuses entirely if we ask exactly, so have to ask for ideal
|
||||
instead
|
||||
XXX: Is this still true?
|
||||
*/
|
||||
width: isWebkit ? { exact: 640 } : { ideal: 640 },
|
||||
height: isWebkit ? { exact: 360 } : { ideal: 360 },
|
||||
}
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
private getScreenshareContraints(desktopCapturerSourceId?: string): DesktopCapturerConstraints {
|
||||
if (desktopCapturerSourceId) {
|
||||
logger.debug("Using desktop capturer source", desktopCapturerSourceId);
|
||||
return {
|
||||
audio: false,
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: desktopCapturerSourceId,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
logger.debug("Not using desktop capturer source");
|
||||
return {
|
||||
audio: false,
|
||||
video: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user